SwiftUI: How to switch only first item of an array - ios

I'm working on iOS App with a card-swipe function like Tinder. I have an array with cards. I want to integrate the possibility to flip the card to get more information.
struct TryView: View {
#ObservedObject var cards = APIObs()
#State var back = false
var body: some View {
GeometryReader{geo in
ZStack{
ForEach(cards.shows){ i in
ZStack{
if back == false{
SwipeDetailsView1(name: i.name, age: i.status, image: "https://image.tmdb.org/t/p/w500\(i.image)", height: geo.size.height-80, info: $back) .rotation3DEffect(.degrees(self.back ? 180.0 : 0.0), axis: (x: 0.0, y: 1.0, z: 0.0))
.zIndex(self.back ? 0 : 1)
}else{
BackView(name: i.name, height: geo.size.height-80, info: $back) .rotation3DEffect(.degrees(self.back ? 0.0 : 180.0), axis: (x: 0.0, y: -1.0, z: 0.0))
.zIndex(self.back ? 1 : 0)
}
}.animation(.easeOut(duration: 0.25))
}
}
}
}
My Problem is, that when back == true all cards of the array flip. I just want the first one to flip. Does anyone know how to manage that?

That is because your #State var back is for the array.
If you want to control each card you need a CardView with an #State var back for each card.
I put some sample code below. A minimum reproducible example is preferred so we don't have to recreate struct's and other components
import SwiftUI
struct TryView: View {
//#ObservedObject var cards = APIObs()//Code not Provided
#State var cards = ["Alpha", "Beta", "Charlie"]
var body: some View {
GeometryReader{geo in
VStack{
ForEach(cards, id: \.self){ i in
CardView(card: i)
}
}
}
}
}
struct SwipeDetailsView1: View {
var name: String
var body: some View {
Text(name)
}
}
struct BackView: View {
var name: String
var body: some View {
Text(name)
}
}
struct CardView: View {
#State var back = false //Variable for just the card
var card: String
var body: some View {
VStack{
ZStack{
if back == false{
SwipeDetailsView1(name: "front \(card)")
}else{
BackView(name: "back \(card)")
}
}.animation(.easeOut(duration: 0.25))
Button("flip", action: {
back.toggle()
})
}
}
}
struct TryView_Previews: PreviewProvider {
static var previews: some View {
TryView()
}
}

Related

SwiftUI. Subview animation is not working if subview's View #State was changed while parent View is animating. iOS 16

I have a structure, where the parent view is a hidden pop-up with content. On some user action parent View starts to slide up. I need all its content to slide up with the parent View. It was working perfectly before iOS 16, but now it's broken. If the child's View subview #State is changed during the animation, then this View appears instantly on a screen, i.e. not sliding. As I understand, because View's #State was changed SwiftUI redraws this particular View and disables its animation, so it appears in its final position without animation. I was trying to force the animation of this particular View using .animation or withAnimation. Nothing of it helped. How to fix this bug in iOS 16?
Minimal reproducible example:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
import SwiftUI
struct ContentView: View {
#State var shouldShow: SlideCardPosition = .bottom
#State var text1: String = ""
var body: some View {
VStack {
Button {
shouldShow = .top
} label: {
Text("Show PopUp")
.padding(.top, 100)
}
PopUpUIView(shouldShow: $shouldShow)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct SlideOverCard<Content: View>: View {
#GestureState private var dragState = SlideDragState.inactive
#Binding private var position: SlideCardPosition
#State private var highlightedBackground = false
private var contentHeight: CGFloat
private var backgroundColor: Color?
private var withCorners: Bool
private var isHandleHidden: Bool
private var overlayOpacity: CGFloat
init(position: Binding<SlideCardPosition>,
contentHeight: CGFloat,
backgroundColor: Color? = nil,
withCorners: Bool = true,
isHandleHidden: Bool = false,
overlayOpacity: CGFloat = 0.75,
content: #escaping () -> Content) {
_position = position
self.content = content
self.contentHeight = contentHeight
self.backgroundColor = backgroundColor
self.withCorners = withCorners
self.isHandleHidden = isHandleHidden
self.overlayOpacity = overlayOpacity
}
var content: () -> Content
var body: some View {
return Rectangle()
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight)
.foregroundColor(Color.black.opacity(highlightedBackground ? overlayOpacity : 0))
.position(x: UIScreen.screenWidth / 2, y: (UIScreen.screenHeight) / 2)
.edgesIgnoringSafeArea([.top, .bottom])
.overlay(
Group {
VStack(spacing: 0) {
if !isHandleHidden {
Handle()
}
self.content()
Spacer()
}
}
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight)
.background(backgroundColor != nil ? backgroundColor! : Color.black)
.cornerRadius(withCorners ? 40.0 : 0)
.shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.13), radius: 10.0)
.offset(y: position(from: position) + dragState.translation.height)
.animation(dragState.isDragging ? nil : .interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0), value: UUID())
.edgesIgnoringSafeArea([.top, .bottom])
.onTapGesture {}
.onChange(of: position) { _ in
withAnimation(.easeInOut) {
highlightedBackground.toggle()
}
}
)
.onTapGesture {
position = position == .bottom ? .top : .bottom
}
}
private func position(from cardPosition: SlideCardPosition) -> CGFloat {
switch cardPosition {
case .top: return UIScreen.screenHeight - contentHeight - UIScreen.topSafeAreaHeight
case .bottom: return 1000
}
}
}
enum SlideCardPosition {
case top
case bottom
}
private enum SlideDragState {
case inactive
case dragging(translation: CGSize)
var translation: CGSize {
switch self {
case .inactive:
return .zero
case .dragging(let translation):
return translation
}
}
var isDragging: Bool {
switch self {
case .inactive:
return false
case .dragging:
return true
}
}
}
private struct Handle: View {
private let handleThickness: CGFloat = 5
var body: some View {
RoundedRectangle(cornerRadius: handleThickness / 2.0)
.frame(width: 34, height: handleThickness)
.foregroundColor(.white)
.padding(.top, 8)
}
}
import UIKit
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
private static let window = UIApplication.shared.windows[0]
private static let safeFrame = window.safeAreaLayoutGuide.layoutFrame
static var topSafeAreaHeight: CGFloat {
safeFrame.minY
}
static var bottomSafeAreaHeight: CGFloat {
window.frame.maxY - safeFrame.maxY
}
}
import SwiftUI
struct PopUpUIView: View {
#Binding var shouldShow: SlideCardPosition
#State var text1 = "some random text"
var body: some View {
SlideOverCard(position: $shouldShow,
contentHeight: 300) {
VStack(spacing: 10) {
Text(text1)
.foregroundColor(.white)
.padding(.top, 80)
}
}.onChange(of: shouldShow) { _ in
if shouldShow == .top {
text1 = UUID().uuidString
}
}
}
}
struct PopUpUIView_Previews: PreviewProvider {
static var previews: some View {
PopUpUIView(shouldShow: .constant(.bottom))
}
}
Example of incorrect animation with a dynamic text.
Example of what I want to achieve. It is working fine if text is static.

How to change the picture of one button in a dynamic array of buttons SwiftUI

I have a dynamic array of buttons. By pressing one button, its picture should change, the rest should not change. But when I click on any button, the pictures of all the elements change too.
How to make it so that when you click on a button, the picture of only this button changes, and the rest do not change? Thank you
import SwiftUI
struct Result {
var id = UUID()
var score: Int
}
struct DynamicButtonsView: View {
let results = [Result(score: 8), Result(score: 5), Result(score: 10), Result(score: 12) , Result(score: 33)]
#State var imageName: String = "UnselectedSircle"
var body: some View {
VStack {
ForEach(results, id:(\.id)) { result in
Button(action: {
print(result.score)
print(imageName)
self.imageName = "SelectedCircle"
}, label: {
Image(imageName)
.resizable()
.scaledToFill()
Text("\(result.score)")
}).frame(width: 50, height: 50, alignment: .center)
}
}
}
}
struct DynamicButtonsView_Previews: PreviewProvider {
static var previews: some View {
DynamicButtonsView()
}
}
this code work like charm, tested on iPhone 6s (iOS 14.4)
struct Result {
var id = UUID()
var score: Int
var isSected:Bool = false
}
struct test: View {
#State private var myList = [Result(score: 23),Result(score: 33),Result(score: 28),Result(score: 11)]
var body: some View {
List(myList,id:\.id){item in
HStack{
Text("\(item.score)")
Spacer()
Button(action: {
guard let index = myList.firstIndex(where: {$0.id == item.id})else{
return
}
myList[index].isSected.toggle()
}, label: {
Image(systemName:item.isSected ? "circle.fill" : "circle")
})
}
}
}
}
So, if I understand you correctly, you want any button that is tapped to have its picture change and any button that was not tapped to not change? Pretty much representing some "selection state" of the button?
Basically, you have to model the state you want to see. If only your view needs to know about the selection state, you can model it as a set of UUIDs like this:
struct DynamicButtonsView: View {
let results = [Result(score: 8), Result(score: 5), Result(score: 10), Result(score: 12) , Result(score: 33)]
#State var selectedIDs = Set<UUID>()
#State var imageName: String = "checkmark.circle"
var body: some View {
VStack {
ForEach(results, id:(\.id)) { result in
Button(action: {
if selectedIDs.contains(result.id) {
selectedIDs.remove(result.id)
} else {
selectedIDs.insert(result.id)
}
print(result.score)
print(imageName)
}, label: {
Image(systemName: selectedIDs.contains(result.id) ? "checkmark.circle.fill" : "checkmark.circle")
.resizable()
.scaledToFill()
Text("\(result.score)")
}).frame(width: 50, height: 50, alignment: .center)
}
}
}
}
Note: I changed out the images purely because I don't have your asset catalog, so I used some random images from SFSymbols for my example.

Programmatically drawn circles are not appearing SwiftUI

I am trying to create a test application using SwiftUI where the user can draw on the screen when they drag on the screen. However, I am having some difficulties getting the Circles that I am using to represent the pen to appear.
Here is the ContentView.swift code that I am using.
import SwiftUI
var list_of_points = [CGPoint]()
struct ContentView: View {
var body: some View {
ZStack{
Rectangle().fill(Color.gray)
Text("Hello!")
}.gesture(DragGesture().onChanged({
value in
drag_responder(point: value.location)
}))
}
}
func drag_responder(point: CGPoint){
print("Drawing at \(point)")
list_of_points.append(point)
let pen = Circle().size(CGSize(width:10, height:10)).position(point)
pen
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct Drawing {
var points: [CGPoint] = [CGPoint]()
}
struct ContentView: View {
#State private var currentDrawing: Drawing = Drawing()
#State private var drawings: [Drawing] = [Drawing]()
#State private var color: Color = Color.black
#State private var lineWidth: CGFloat = 3.0
var body: some View {
VStack(alignment: .center) {
DrawingPad(currentDrawing: $currentDrawing,
drawings: $drawings,
color: $color,
lineWidth: $lineWidth)
}
}
}
struct DrawingPad: View {
#Binding var currentDrawing: Drawing
#Binding var drawings: [Drawing]
#Binding var color: Color
#Binding var lineWidth: CGFloat
var body: some View {
GeometryReader { geometry in
Path { path in
for drawing in self.drawings {
self.add(drawing: drawing, toPath: &path)
}
self.add(drawing: self.currentDrawing, toPath: &path)
}
.stroke(self.color, lineWidth: self.lineWidth)
.background(Color(white: 0.95))
.gesture(
DragGesture(minimumDistance: 0.1)
.onChanged({ (value) in
let currentPoint = value.location
if currentPoint.y >= 0
&& currentPoint.y < geometry.size.height {
self.currentDrawing.points.append(currentPoint)
}
})
.onEnded({ (value) in
self.drawings.append(self.currentDrawing)
self.currentDrawing = Drawing()
})
)
}
.frame(maxHeight: .infinity)
}
private func add(drawing: Drawing, toPath path: inout Path) {
let points = drawing.points
if points.count > 1 {
for i in 0..<points.count-1 {
let current = points[i]
let next = points[i+1]
path.move(to: current)
path.addLine(to: next)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How to change the offset of an view using onTapGesture?

I am trying to move a view when it is being tapped. But unfortunately nothing happens. How do I toggle the position of it using the offset y axis?
struct Test: View {
#State var cards : [Card] = [Card(), Card(), Card(), Card(), Card(), Card()]
var body: some View {
VStack {
ZStack (alignment: .top){
ForEach (0..<cards.count) { i in
RoundedRectangle(cornerRadius: 10).frame(width: 250, height: 150)
.foregroundColor(Color.random).offset(y: self.cards[i].isFocus ? CGFloat(500) : CGFloat(i*50))
.zIndex(Double(i))
.onTapGesture {
self.cards[i].isFocus.toggle() // now the touched Card should move to an offset of y: 500, but nothing happens
}
}
}
Spacer()
}
}
}
struct Card :Identifiable{
#State var isFocus :Bool = false
var id = UUID()
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
extension Color {
static var random: Color {
return Color(red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1))
}
}
I see your code and I am sorry to say but its wrong in many aspects. I would recommend you to go through SwiftUI basics and understand how and when we use #State, #ObservedObject, #Binding etc. Also always try to break your Views into the smallest components.
Below is the code which I think fulfills your requirement.
struct Test: View {
var cards : [Card] = [Card(), Card(), Card(), Card(), Card(), Card()]
var body: some View {
VStack {
ZStack (alignment: .top){
ForEach (0..<cards.count) { i in
CardView(card: self.cards[i], i: i)
}
}
Spacer()
}
}
}
struct CardView: View {
#ObservedObject var card: Card
let i: Int
var body: some View {
RoundedRectangle(cornerRadius: 10).frame(width: 250, height: 150)
.foregroundColor(Color.random)
.offset(y: card.isFocus ? CGFloat(500) : CGFloat(i*10))
.zIndex(Double(i))
.onTapGesture {
withAnimation {
self.card.isFocus.toggle()
}
}
}
}
class Card: ObservableObject{
#Published var isFocus: Bool = false
var id = UUID()
}

SwiftUI Card flip with two views

I am trying to create a card flip effect between two SwiftUI Views. When clicking on the original view, it 3D rotates on the Y axis like when flipping a card, and the second view should start being visible after 90 degrees have been made.
Using .rotation3DEffect() I can easily rotate a view, the issue is that with the animation() I don't know how to trigger the View change once the angle has reached 90 degrees...
#State var flipped = false
var body: some View {
return VStack{
Group() {
if !self.flipped {
MyView(color: "Blue")
} else {
MyView(color: "Red")
}
}
.animation(.default)
.rotation3DEffect(self.flipped ? Angle(degrees: 90): Angle(degrees: 0), axis: (x: CGFloat(0), y: CGFloat(10), z: CGFloat(0)))
.onTapGesture {
self.flipped.toggle()
}
}
How to achieve such a rotation between two views ?
Simple Solution
The approach you're taking can be made to work by putting your two views in a ZStack and then showing/hiding them as the flipped state changes. The rotation of the second view needs to be offset. But this solution relies on a cross-fade between the two views. It might be OK for some uses cases. But there is a better solution - though it's a bit more fiddly (see below).
Here's a way to make your approach work:
struct SimpleFlipper : View {
#State var flipped = false
var body: some View {
let flipDegrees = flipped ? 180.0 : 0
return VStack{
Spacer()
ZStack() {
Text("Front").placedOnCard(Color.yellow).flipRotate(flipDegrees).opacity(flipped ? 0.0 : 1.0)
Text("Back").placedOnCard(Color.blue).flipRotate(-180 + flipDegrees).opacity(flipped ? 1.0 : 0.0)
}
.animation(.easeInOut(duration: 0.8))
.onTapGesture { self.flipped.toggle() }
Spacer()
}
}
}
extension View {
func flipRotate(_ degrees : Double) -> some View {
return rotation3DEffect(Angle(degrees: degrees), axis: (x: 1.0, y: 0.0, z: 0.0))
}
func placedOnCard(_ color: Color) -> some View {
return padding(5).frame(width: 250, height: 150, alignment: .center).background(color)
}
}
Better Solution SwiftUI has some useful animation tools - such as GeometryEffect - that can generate a really smooth version of this effect. There are some excellent blog posts on this topic at SwiftUI Lab. In particular, see: https://swiftui-lab.com/swiftui-animations-part2/
I've simplified and adapted one of examples in that post to provide the card flipping functionality.
struct FlippingView: View {
#State private var flipped = false
#State private var animate3d = false
var body: some View {
return VStack {
Spacer()
ZStack() {
FrontCard().opacity(flipped ? 0.0 : 1.0)
BackCard().opacity(flipped ? 1.0 : 0.0)
}
.modifier(FlipEffect(flipped: $flipped, angle: animate3d ? 180 : 0, axis: (x: 1, y: 0)))
.onTapGesture {
withAnimation(Animation.linear(duration: 0.8)) {
self.animate3d.toggle()
}
}
Spacer()
}
}
}
struct FlipEffect: GeometryEffect {
var animatableData: Double {
get { angle }
set { angle = newValue }
}
#Binding var flipped: Bool
var angle: Double
let axis: (x: CGFloat, y: CGFloat)
func effectValue(size: CGSize) -> ProjectionTransform {
DispatchQueue.main.async {
self.flipped = self.angle >= 90 && self.angle < 270
}
let tweakedAngle = flipped ? -180 + angle : angle
let a = CGFloat(Angle(degrees: tweakedAngle).radians)
var transform3d = CATransform3DIdentity;
transform3d.m34 = -1/max(size.width, size.height)
transform3d = CATransform3DRotate(transform3d, a, axis.x, axis.y, 0)
transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)
let affineTransform = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))
return ProjectionTransform(transform3d).concatenating(affineTransform)
}
}
struct FrontCard : View {
var body: some View {
Text("One thing is for sure – a sheep is not a creature of the air.").padding(5).frame(width: 250, height: 150, alignment: .center).background(Color.yellow)
}
}
struct BackCard : View {
var body: some View {
Text("If you know you have an unpleasant nature and dislike people, this is no obstacle to work.").padding(5).frame(width: 250, height: 150).background(Color.green)
}
}
Update
The OP asks about managing the flip status outside of the view. This can be done by using a binding. Below is a fragment that implements and demos this. And OP also asks about flipping with and without animation. This is a matter of whether changing the flip state (here with the showBack var) is done within an animation block or not. (The fragment doesn't include FlipEffect struct which is just the same as the code above.)
struct ContentView : View {
#State var showBack = false
let sample1 = "If you know you have an unpleasant nature and dislike people, this is no obstacle to work."
let sample2 = "One thing is for sure – a sheep is not a creature of the air."
var body : some View {
let front = CardFace(text: sample1, background: Color.yellow)
let back = CardFace(text: sample2, background: Color.green)
let resetBackButton = Button(action: { self.showBack = true }) { Text("Back")}.disabled(showBack == true)
let resetFrontButton = Button(action: { self.showBack = false }) { Text("Front")}.disabled(showBack == false)
let animatedToggle = Button(action: {
withAnimation(Animation.linear(duration: 0.8)) {
self.showBack.toggle()
}
}) { Text("Toggle")}
return
VStack() {
HStack() {
resetFrontButton
Spacer()
animatedToggle
Spacer()
resetBackButton
}.padding()
Spacer()
FlipView(front: front, back: back, showBack: $showBack)
Spacer()
}
}
}
struct FlipView<SomeTypeOfViewA : View, SomeTypeOfViewB : View> : View {
var front : SomeTypeOfViewA
var back : SomeTypeOfViewB
#State private var flipped = false
#Binding var showBack : Bool
var body: some View {
return VStack {
Spacer()
ZStack() {
front.opacity(flipped ? 0.0 : 1.0)
back.opacity(flipped ? 1.0 : 0.0)
}
.modifier(FlipEffect(flipped: $flipped, angle: showBack ? 180 : 0, axis: (x: 1, y: 0)))
.onTapGesture {
withAnimation(Animation.linear(duration: 0.8)) {
self.showBack.toggle()
}
}
Spacer()
}
}
}
struct CardFace<SomeTypeOfView : View> : View {
var text : String
var background: SomeTypeOfView
var body: some View {
Text(text)
.multilineTextAlignment(.center)
.padding(5).frame(width: 250, height: 150).background(background)
}
}
A cleaned up and extendable solution
Note that you can easily change flip direction by changing axis parameter in .rotation3DEffect.
import SwiftUI
struct FlipView<FrontView: View, BackView: View>: View {
let frontView: FrontView
let backView: BackView
#Binding var showBack: Bool
var body: some View {
ZStack() {
frontView
.modifier(FlipOpacity(percentage: showBack ? 0 : 1))
.rotation3DEffect(Angle.degrees(showBack ? 180 : 360), axis: (0,1,0))
backView
.modifier(FlipOpacity(percentage: showBack ? 1 : 0))
.rotation3DEffect(Angle.degrees(showBack ? 0 : 180), axis: (0,1,0))
}
.onTapGesture {
withAnimation {
self.showBack.toggle()
}
}
}
}
private struct FlipOpacity: AnimatableModifier {
var percentage: CGFloat = 0
var animatableData: CGFloat {
get { percentage }
set { percentage = newValue }
}
func body(content: Content) -> some View {
content
.opacity(Double(percentage.rounded()))
}
}

Resources