Why won't my UI reflect a change in the #State property? - swiftui

I'm trying to make a graph app, but animating it using a #State property does not help, for some reason.
struct GraphBars: View {
#State var percent: CGFloat
var body: some View {
Capsule()
.fill(Color.black)
.frame(width: 50, height: self.percent)
}
}
struct TEST3: View {
#State var bar1: CGFloat = 90.0
var body: some View {
GeometryReader { gg in
VStack {
Button(action: {
self.bar1 = 300.0
}) {
Text("Hello")
}
GraphBars(percent: bar1)
}
However, pressing the button does not increase the height of the bar as I thought it would. What am I doing wrong?

You need to use #Binding to transfer a variable back and forth between two Views otherwise the parent View doesn't get a notice of the variable change.
Do it like this:
struct GraphBars: View {
#Binding var percent: CGFloat
var body: some View {
Capsule()
.fill(Color.black)
.frame(width: 50, height: self.percent)
}
}
struct Test3: View {
#State var bar1: CGFloat = 90.0
var body: some View {
GeometryReader { gg in
VStack {
Button(action: {
self.bar1 = 300.0
}) {
Text("Hello")
}
GraphBars(percent: self.$bar1)
}
}
}
}

Related

SwiftUI gesture state is reset between gestures

I have the following code for a simple square to which I attach a MagnificationGesture to update its state with a pinch to zoom gesture.
import SwiftUI
struct ContentView2: View {
var scale: CGFloat = 1
var magnificationGesture: some Gesture {
MagnificationGesture()
.onChanged { value in
scale = value
}
}
var body: some View {
VStack {
Text("\(scale)")
Spacer()
Rectangle()
.fill(Color.red)
.scaleEffect(self.scale)
.gesture(
magnificationGesture
)
Spacer()
}
}
}
struct ContentView2_Previews: PreviewProvider {
static var previews: some View {
ContentView2()
}
}
However this simple view behaves weird. When I perform the gesture, the scale #State property is succesfully modified. However when I then do another gesture with my hands, the scale property is reset to its initial state, instead of starting from its current value.
Here is a video of what happens. For example, when the red view is very small, performing the gesture, I would expect that it stays small, instead of completely resetting.
How can I get the desired behaviour - that is - the scale property is not reset
I was able to get it working by adding a bit to the code. Check it out and let me know if this works for your use case:
import SwiftUI
struct ContentView2: View {
var magGesture = MagnificationGesture()
#State var magScale: CGFloat = 1
#State var progressingScale: CGFloat = 1
var magnificationGesture: some Gesture {
magGesture
.onChanged { progressingScale = $0 }
.onEnded {
magScale *= $0
progressingScale = 1
}
}
var body: some View {
VStack {
Text("\(magScale)")
Spacer()
Rectangle()
.fill(Color.red)
.scaleEffect(self.magScale * progressingScale)
.gesture(
magnificationGesture
)
Spacer()
}
}
}
struct ContentView2_Previews: PreviewProvider {
static var previews: some View {
ContentView2()
}
}
I solved it by adding another scale and only updating one of them at the end to keep track of the scale
Code
import SwiftUI
struct ContentView2: View {
#State var previousScale: CGFloat = 1
#State var newScale: CGFloat = 1
var magnificationGesture: some Gesture {
MagnificationGesture()
.onChanged { value in
newScale = previousScale * value
}
.onEnded { value in
previousScale *= value
}
}
var body: some View {
VStack {
Text("\(newScale)")
Spacer()
Rectangle()
.fill(Color.red)
.scaleEffect(newScale)
.gesture(
magnificationGesture
)
Spacer()
}
}
}
struct ContentView2_Previews: PreviewProvider {
static var previews: some View {
ContentView2()
}
}

SwiftUI How to Make Page TabView Dynamic

So I am trying to make my TabView height dynamic. I have been looking for a way to do this but I can't seem to find a solution anywhere. This is how my code looks like.
struct ContentView: View {
#State var contentHeight: CGFloat = 0
var body: some View {
NavigationView {
ScrollView {
VStack {
TabView {
TestView1(contentHeight: $contentHeight)
TestView2(contentHeight: $contentHeight)
}
.tabViewStyle(.page)
.frame(height: contentHeight)
.indexViewStyle(.page(backgroundDisplayMode: .always))
.background(.yellow)
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Test Project")
}
}
}
}
This is how my test1view and test2view look like.
struct TestView1: View {
#State var height: CGFloat = 0
#Binding var contentHeight: CGFloat
var body: some View {
Color.red
.frame(maxWidth:.infinity, minHeight: 200, maxHeight: 200)
.background(
GeometryReader { geo in
Color.clear
.preference(
key: HeightPreferenceKey.self,
value: geo.size.height
)
.onAppear {
contentHeight = height
}
}
.onPreferenceChange(HeightPreferenceKey.self) { height in
self.height = height
}
)
}
}
struct TestView2: View {
#Binding var contentHeight: CGFloat
#State var height: CGFloat = 0
var body: some View {
Color.black
.frame(maxWidth:.infinity, minHeight: 350, maxHeight: 350)
.background(
GeometryReader { geo in
Color.clear
.preference(
key: HeightPreferenceKey.self,
value: geo.size.height
)
.onAppear {
contentHeight = height
}
}
.onPreferenceChange(HeightPreferenceKey.self) { height in
self.height = height
}
)
}
}
struct HeightPreferenceKey: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
Now the problem is that when I drag it just a little the height changes. So when I drag it a little to the left the height changes to the height of TestView2 and it is still on TestView1.
I tried to add a drag gesture but it didn't let me swipe to the next page. So I don't know how I will be able to achieve this. Ive been looking for a solution but still no luck.
You can use the TabView($selection) initializer to do this. https://developer.apple.com/documentation/swiftui/tabview/init(selection:content:)
It tells you which tab you're currently viewing. Based off the middle point of the screen. And you don't have to deal with nasty GeometryReader and HeightPreferenceKey.
Here's your updated code. I even added a nice animation to fade between the two heights!
struct ContentView: View {
#State var selectedTab: Tab = .first
#State var animatedContentHeight: CGFloat = 300
enum Tab {
case first
case second
var contentHeight: CGFloat {
switch self {
case .first:
return 200
case .second:
return 350
}
}
}
var body: some View {
TabView(selection: $selectedTab) {
TestView1()
.tag(Tab.first)
TestView2()
.tag(Tab.second)
}
.tabViewStyle(.page)
// .frame(height: selectedTab.contentHeight) // Uncomment to see without animation
.frame(height: animatedContentHeight)
.indexViewStyle(.page(backgroundDisplayMode: .always))
.onChange(of: selectedTab) { newValue in
print("now selected:", newValue)
withAnimation { animatedContentHeight = selectedTab.contentHeight }
}
}
}
struct TestView1: View {
var body: some View {
Color.red
}
}
struct TestView2: View {
var body: some View {
Color.black
}
}

SwiftUI: is possible Gesture var move into EnvironmentObject?

I am not familiar with SWIFTUI and closure. Here move some gesture code to extension function, except var drag.
For Gesture, Is is possible to move the drag into environmentObject? How to initialize ?
class ViewState:ObservableObject {
#Published var isDragging = false
#Published var size: CGFloat = 100.0
}
struct ContentView: View {
#StateObject var viewState:ViewState=ViewState()
var drag:some Gesture { isDrag(viewState) }
var body: some View {
Circle()
.fill(viewState.isDragging ? Color.red: Color.blue)
.frame(width:viewState.size, height:viewState.size, alignment: .center)
.gesture(drag)
}
}
extension ContentView {
func isDrag(_ viewState:ViewState)->some Gesture {
DragGesture()
.onChanged {_ in
viewState.isDragging = true
viewState.size += 10.0
}
.onEnded{_ in
viewState.isDragging = false
viewState.size = 100.0
}
}
}
Done, Closure is function! Now, I can move this code to other views, but still update with view risk. and declared data (environmentObject) is not independent.
class ViewState:ObservableObject {
#Published var isDragging = false
#Published var size: CGFloat = 100.0
}
struct ContentView: View {
#StateObject var viewState:ViewState=ViewState()
// var drag:some Gesture { }
var body: some View {
Circle()
.fill(viewState.isDragging ? Color.red: Color.blue)
.frame(width:viewState.size, height:viewState.size, alignment: .center)
.isDragged(viewState)
}
}
extension View {
func drag(_ viewState:ViewState)->some Gesture {
DragGesture()
.onChanged {_ in
viewState.isDragging = true
viewState.size += 10.0
}
.onEnded{_ in
viewState.isDragging = false
viewState.size = 100.0
}
}
#ViewBuilder func isDragged(_ viewState:ViewState) -> some View {
self.gesture(drag(viewState))
}
}

SwiftUI different actions on the same Custom Alert actions

I created a custom alert. I want the product to be added to the basket when the Ok button on Alert is clicked on the first screen. When the Ok button is pressed on the second screen, the purchase of the product is requested. I called the same alert on 2 pages and I want it to take different actions. I couldn't do that with #Escaping.
AlertView
struct AlertView: View {
#Binding var openShowAlert: Bool
#State var closeShowAlert: Bool = false
#State var openState: CGFloat = -UIScreen.main.bounds.height
#State var closeState: CGFloat = UIScreen.main.bounds.height
var title: String = ""
var message: String = ""
var okButtonText: String = ""
var cancelButtonText: String = ""
var body: some View {
VStack {
Text(title)
.michromaFont(size: 20)
.padding(.top)
Spacer()
Text(message)
.michromaFont(size: 18)
Spacer()
HStack {
Button(action: {
self.openShowAlert = false
openState = -UIScreen.main.bounds.height
closeState = UIScreen.main.bounds.height
}) {
Text(cancelButtonText)
.foregroundColor(.red)
}
Spacer()
Button(action: {}) {
Text(okButtonText)
}
}
.michromaFont(size: 18)
.padding([.horizontal, .bottom])
}
.neumorphisimBackground(width: 300, height: 200)
.offset(y: self.openShowAlert ? self.openState : self.closeState)
.animation(.easeInOut)
.onChange(of: self.openShowAlert, perform: { value in
if value {
self.openState = .zero
}
})
}
}
DetailView
On this screen, click Alert presentation to add the product to the cart.
struct DetailView: View {
#Environment(\.presentationMode) var presentationMode
var device = UIDevice.current.userInterfaceIdiom
#State var width: CGFloat = 300
#State var height: CGFloat = 450
#Binding var text: String
#State var showAlert: Bool = false
var body: some View {
ZStack() {
......
AlertView(openShowAlert: self.$showAlert)
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
CartView Click I am providing an alert on this screen to purchase the product.
struct CartView: View {
#State var cartList = [1,2,3,4,5,6,7,8,9]
#Environment(\.presentationMode) var presentationMode
#State var showAlert: Bool = false
var body: some View {
ZStack(alignment: .top) {
.....
AlertView(openShowAlert: self.$showAlert)
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
How can I send two different actions in the same alert.
Hmm, I don't see why it shouldn't work with a closure. Have you tried passing over a closure like so?
struct AlertView: View {
...
var okButtonAction: () -> ()
var body: some View {
...
Button(action: okButtonAction) {
Text(okButtonText)
}
}
}
Usage
AlertView(openShowAlert: self.$showAlert) {
// Your custom code
}
Alternative Idea
You could work with Combine and create a publisher with a specific key to identify the sender screen. Then you can put your custom code inside .onReceive().

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