Swap two elements onTapGesture with zIndex updated for the animated transition - ios

I use matchedGeometryEffect and TapGesture to swap two elements with animated transition.
The animation works as expected but I want to change the zIndex of the elements for the duration of the animation.
I started from this example : https://www.appcoda.com/matchedgeometryeffect/
and adapted it to my needs :
struct MyView: View {
#State var colors = [Color.orange,Color.red,Color.yellow,Color.pink,Color.blue]
#State var target = Int.random(in: 0..<4)
#Namespace private var animationNamespace : Namespace.ID
var body: some View {
let columns: [GridItem] = Array(repeating: .init(.fixed(80)), count: 2)
LazyVGrid(columns: columns, spacing: 5){
ForEach(Array(self.colors.enumerated()), id: \.self.element) { indexedColor in
Element(color: indexedColor.element, animationNamespace: self.animationNamespace, action: {
self.colors.swapAt(target, indexedColor.offset)
target = Int.random(in: 0..<4)
})
}
}.animation(.linear)
}
}
struct Element: View {
let color : Color
let animationNamespace : Namespace.ID
let action: () -> Void
var body: some View {
Circle()
.fill(color)
.frame(width: 80, height: 80)
.matchedGeometryEffect(id: color, in: animationNamespace)
.onTapGesture {
action()
}
}
}
I want the animated elements to be always on top of the other elements.
The zIndex must be reseted at the end of the animation
Is this possible by using TapGesture ?
Xcode Version 12.0.1 (12A7300)
iOS 14.0

It is possible to put the clicked dot on the top level.. I changed your code here..
struct ContentView: View {
#State var colors = [Color.orange,Color.red,Color.yellow,Color.pink,Color.blue]
#Namespace private var animationNamespace : Namespace.ID
var body: some View {
HStack{
ForEach(Array(self.colors.enumerated()), id: \.self.element) { indexedColor in
Element(color: indexedColor.element, animationNamespace: self.animationNamespace, order: indexedColor.0, action: {
self.colors.swapAt(0, indexedColor.offset)
})
}
}.animation(.linear)
}
}
struct Element: View {
let color : Color
let animationNamespace : Namespace.ID
var order : Int
let action: () -> Void
var body: some View {
Circle()
.fill(color)
.frame(width: 80, height: 80)
.matchedGeometryEffect(id: color, in: animationNamespace)
.zIndex(Double(5-order))
.onTapGesture {
action()
}
}
}
Basically I just put the zIndex for each element based on their position. This means the first dot will have the highest zIndex. If you change the positions, the clicked one will get the highest zIndex and will lay on top of each other. I think it is not possible to put the other one on top aswell, as it will get the other zIndex, which was the lower one.

Related

Access a variable in another view to reset it

I will try to explain my project, I develop an app on iPad.
First I have an HStack with a series of buttons which contain data, the button are green at the beginning and when we tap on it the button becomes red and the data contained in the button are stored in a variable.
Then, I have a second series of buttons which contain initial data, which must be replaced by other data (the data contained in the variable when we tap on a button which is in the HStack).
So we tap on the first button the data is recorded in a variable and we tap on a second button where we want the data.
The problem is that after that I want to delete the button contained in HStack this I can do it. But the red color stay on another button.
I suppose that it is because the "didTap" variable remains at true but how Can I "reset" this variable (becomes again false) when I tap on the second button.
Here is a simplified version of the code :
import SwiftUI
struct ContentView: View {
#State private var allWords : [String] = ["test1", "test2", "test3", "test4", "test5", "test6"] // contain the data to put in the buttons which are in the HStack
#State private var textInButton : String = "" //text temporarly stored
#State private var arrayIndice : Int = 0 //index temporarly stored
#State private var oneTap : Bool = false
#State private var isNight = false //unused at this moment
private var col : [GridItem] = Array(repeating: .init(.fixed(45)), count: 18)
let spaceOriginal = ["A1","A2","B1"]
#State var space = ["A1","A2","B1"]
var body: some View {
VStack{
ScrollView(.horizontal) {
HStack{
ForEach(allWords.indices, id: \.self) { index in
// display list of buttons ...
MyButton(myRow: allWords[index], oneTap: $oneTap, isNight: $isNight, textInButton: $textInButton, arrayIndice: $arrayIndice, arrayLocalIndice : index, allWords: $allWords)
}
}
.padding(.bottom, 30)
}
LazyVGrid(columns: col, spacing: 0) {
ForEach(0..<space.count) { i in
// display some button that I want to fill with the data contained in the buttons which are in the HStack
Button(action: {
// display again the original data when we click on the button and want to "reset" the button
if (space[i] != spaceOriginal[i]){
allWords.append(space[i])
space[i] = spaceOriginal[i]
}
// put data which are in the selected button (located in the hstack) in the button which I have to fill
if(textInButton != "" && space[i] == spaceOriginal[i]){
space[i] = textInButton
textInButton = ""
allWords.remove(at: arrayIndice)
oneTap = false
}
}, label: {
Text("\(space[i])")
.frame(width:50, height:75)
.font(.system(size: 20, weight: .medium))
.border(Color.black, width: 1)
})
}
}
}
}
}
struct ULD: View{
var title: String
var textColor: Color
var backgroundColor: Color
var body: some View{
Text(title)
.frame(width:75, height:75)
.background(backgroundColor)
.font(.system(size: 10, weight: .medium))
.foregroundColor(textColor)
.border(Color.black, width: 1)
}
}
struct MyButton: View {
#State private var didTap = false
var myRow: String
#Binding var oneTap: Bool
#Binding var isNight: Bool
#Binding var textInButton: String
#Binding var arrayIndice: Int
var arrayLocalIndice : Int
#Binding var allWords: [String]
var body: some View {
Button{
// reset the button if I tap again on a button located in the Hstack
if (didTap == true){
didTap.toggle()
oneTap = false
textInButton = ""
arrayIndice = 0
}
// tap on the button
else if (oneTap == false){
oneTap = true
didTap.toggle()
textInButton = myRow
arrayIndice = arrayLocalIndice
isNight.toggle()
}
} label:{
// By default the button is green, when we tap the first time on the button the didTap variable becomes true and the button becomes red, if we tap again it becomes green again
ULD(title: myRow, textColor: .black, backgroundColor: didTap ? .red : .green)
}
}
}
Here is some pictures to illustrate the problem :
Initial state :
When I click on test 2 it becomes red :
When I click on the position A2 the button is filled with test 2 but test 3 (the second position in the HStack remains red) :
Thank you very much

Text view in SwiftUI doesn't show only with bigger font

In DoctorHomePage I have a grouped list and above the list I want to add a text view, but the text view doesn't show only if I change the font to a bigger one, but it is too big and I want it smaller. Here is my code:
import SwiftUI
import Combine
struct DoctorHomePage: View {
#Binding var shouldPopToRootView : Bool
#State private var curent: Int? = nil
#State private var isActive: Bool = false
#State private var id = 0
let defaults = UserDefaults.standard
let networkRequest = Network()
#State var cancelable: AnyCancellable? = nil
#State var localPatients : [Patients] = []
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: ContentView(), tag: 1, selection: $curent) {
EmptyView()
}
Text("Welcome, doctor!") // this is the text that I want to add
.font(.system(size: 30)).fontWeight(.ultraLight)
.padding(.top, 50)
// PATIENT LIST
List(localPatients) { patient in
VStack(alignment: .leading) {
Text(patient.name)
}
}.listStyle(GroupedListStyle())
.onAppear(perform: {
self.loadPatients()
connCode = self.defaults.integer(forKey: "doctorID")
self.id = connCode
})
}.edgesIgnoringSafeArea([.top, .bottom])
}.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}
Here are some screen shots to help you understand the problem:
The first image is with no text view.
The second image is with the font size of 60.
The third image is with the font size of 30.
Seems like some strange / buggy behavior.
Setting the zIndex of you welcome text will fix your problem.
Text("Welcome, doctor!").zIndex(1)

SwiftUI drag gesture across multiple subviews

I'm attempting to create a grid of small square views, that when the user hovers over them with their thumb (or swipes across them), the little squares will temporarily "pop up" and shake. Then, if they continue to long press on that view, it would open up another view with more information.
I thought that implementing a drag gesture on the square views would be enough, but it looks like only one view can capture a drag gesture at a time.
Is there way to enable multiple views to capture a drag gesture, or a way to implement a "hover" gesture for iOS?
Here is my main Grid view:
import SwiftUI
struct ContentView: View {
#EnvironmentObject var data: PlayerData
var body: some View {
VStack {
HStack {
PlayerView(player: self.data.players[0])
PlayerView(player: self.data.players[1])
PlayerView(player: self.data.players[2])
}
HStack {
PlayerView(player: self.data.players[3])
PlayerView(player: self.data.players[4])
PlayerView(player: self.data.players[5])
}
HStack {
PlayerView(player: self.data.players[6])
PlayerView(player: self.data.players[7])
PlayerView(player: self.data.players[8])
}
HStack {
PlayerView(player: self.data.players[9])
PlayerView(player: self.data.players[10])
}
}
}
}
And here is my Square view that would hold a small summary to display on the square:
import SwiftUI
struct PlayerView: View {
#State var scaleFactor: CGFloat = 1.0
var player: Player = Player(name: "Phile", color: .green, age: 42)
var body: some View {
ZStack(alignment: .topLeading) {
Rectangle().frame(width: 100, height: 100).foregroundColor(player.color).cornerRadius(15.0).scaleEffect(self.scaleFactor)
VStack {
Text(player.name)
Text("Age: \(player.age)")
}.padding([.top, .leading], 10)
}.gesture(DragGesture().onChanged { _ in
self.scaleFactor = 1.5
}.onEnded {_ in
self.scaleFactor = 1.0
})
}
}
Here is a demo of possible approach... (it is simplified version of your app data settings, but the idea and direction where to evolve should be clear)
The main idea that you capture drag not in item view but in the content view transferring needed states (or calculable dependent data) into item view when (or if) needed.
struct PlayerView: View {
var scaled: Bool = false
var player: Player = Player(name: "Phile", color: .green, age: 42)
var body: some View {
ZStack(alignment: .topLeading) {
Rectangle().frame(width: 100, height: 100).foregroundColor(player.color).cornerRadius(15.0).scaleEffect(scaled ? 1.5 : 1)
VStack {
Text(player.name)
Text("Age: \(player.age)")
}.padding([.top, .leading], 10)
}.zIndex(scaled ? 2 : 1)
}
}
struct ContentView: View {
#EnvironmentObject var data: PlayerData
#GestureState private var location: CGPoint = .zero
#State private var highlighted: Int? = nil
private var Content: some View {
VStack {
HStack {
ForEach(0..<3) { i in
PlayerView(scaled: self.highlighted == i, player: self.data.players[i])
.background(self.rectReader(index: i))
}
}
.zIndex((0..<3).contains(highlighted ?? -1) ? 2 : 1)
HStack {
ForEach(3..<6) { i in
PlayerView(scaled: self.highlighted == i, player: self.data.players[i])
.background(self.rectReader(index: i))
}
}
.zIndex((3..<6).contains(highlighted ?? -1) ? 2 : 1)
}
}
func rectReader(index: Int) -> some View {
return GeometryReader { (geometry) -> AnyView in
if geometry.frame(in: .global).contains(self.location) {
DispatchQueue.main.async {
self.highlighted = index
}
}
return AnyView(Rectangle().fill(Color.clear))
}
}
var body: some View {
Content
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.updating($location) { (value, state, transaction) in
state = value.location
}.onEnded {_ in
self.highlighted = nil
})
}
}

#Binding properties are not refresh view of Child View in SwiftUI

I'm trying to reusable View and I added it on ContentView
This is my Child View
struct VStackView: View {
#Binding var spacing: Double
#Binding var alignmentIndex: Int
#Binding var elementsCount: Int
private let alignments: [HorizontalAlignment] = [.leading, .center, .trailing]
var body: some View {
VStack(alignment: self.alignments[alignmentIndex], spacing: CGFloat(spacing)) {
ForEach(0..<elementsCount) {
Text("\($0)th View")
}
}
}
}
and This is SuperView
Superview has Controls like Stepper, Slider, Picker that adjust values of VStack (alignment, spacing etc)
and I want to show the result depending on that values. but Child View is not changed
struct LayoutView: View {
private let layout: StackLayout
#State private var spacing = 0.0
#State private var alignmentIndex = 0
#State private var alignment: HorizontalAlignment = .leading
#State private var elementsCount: Int = 0
private let alignmentsString = [".leading", ".center", ".trailing"]
private let minValue = 0.0
private let maxValue = 100.0
init(_ layout: StackLayout) {
self.layout = layout
}
var body: some View {
NavigationView {
Form {
Section(header: Text("Controls")) {
VStack(alignment: .leading) {
Text("Spacing: \(Int(spacing))").font(.caption)
HStack {
Text("\(Int(minValue))")
Slider(value: $spacing, in: minValue...maxValue, step: 1)
Text("\(Int(maxValue))")
}
Divider()
Picker("alignment", selection: $alignmentIndex) {
ForEach(0..<self.alignmentsString.count) {
Text("\(self.alignmentsString[$0])")
}
}.pickerStyle(SegmentedPickerStyle())
Divider()
Stepper(value: $elementsCount, in: 0...10) {
Text("Element Count: \(elementsCount)")
}
}
}
VStackView(spacing: $spacing, alignmentIndex: $alignmentIndex, elementsCount: $elementsCount)
}
.navigationBarTitle(Text(layout.rawValue), displayMode: .inline)
}
}
}
I also search google and they recommend #EnviornmentObject. if that is correct, when to use #Binding property wrapper.
Isn't it two way binding properties?
Simply speaking you can use #Binding, when you want to share data in two places.
#Observable or #environmetobject is to be used, when you want to share your data in multiple views.
Your ForEach Loop in the VStackView generates a problem, because Swiftui does not know how it can identify each of your items uniquely so it does not know how to update them, when values change.
Append your code like this:
ForEach(0..<elementsCount, id: \.self) {
Text("\($0)th View")
}

Customized segmented control in Swift?

I would like to recreate the same effect than Pinterest:
Given that I'm new in Swift, I have three simple questions:
Is the menu a segmented control customized? or something like buttons?
How can I create this effect/animation of sliding? Is this a collectionView or something like that?
And finally, is it possible to create this with storyboard or the viewController needs to be created in full code ?
my two cents for highly customisable segmented, with colors and fonts.
struct ContentView: View {
#State private var segmentedSelection = 0
let titles = ["AA", "BB", "CC"]
let colors = [Color.red, .green, .white]
var body: some View {
VStack {
CustomSegmentedControl(segmentedSelection: $segmentedSelection, titles: titles, colors: colors)
Spacer()
Text("Hello, selection is " + titles[segmentedSelection] )
.padding()
}
}
}
struct CustomSegmentedControl: View {
#Binding var segmentedSelection : Int
var titles : [String]
let colors : [Color]
var body: some View {
VStack {
HStack{
ForEach(0 ..< titles.count, id: \.self){ (i: Int) in
Button(action: {
print(titles[i])
segmentedSelection = i
}, label: {
Text(titles[i])
.foregroundColor(colors[i])
.font(.system(size: i == segmentedSelection ? 22 : 16))
})
.frame(minWidth: 0, maxWidth: .infinity)
}
}
}
}
}

Resources