I'm practicing swiftui geometryEffect by applying it to a transition from view to another. The first view has three circles with different colors, the user selects a color by tapping the desired color, and clicks "next" to go to second view which contains a circle with the selected color.
GeometryEffect works fine when transitioning from FirstView to SecondView in which the selected circle's positions animates smoothly, but going back is the problem.
GeometryEffect does not animate the circle position smoothly when going back from SecondView to FirstView. Instead, it seems like it moves the circle to the left and right before positioning the circle to its original position.
Im sharing a GIF on my google drive to show what I mean:
I'd like to achieve something like this: desired result
(file size is too large to be uploaded directly)
Thank you!
FirstView:
struct FirstView: View {
#Namespace var namespace
#State var whichStep: Int = 1
#State var selection: Int = 0
var colors: [Color] = [.red, .green, .blue]
#State var selectedColor = Color.black
var transitionNext: AnyTransition = .asymmetric(
insertion: .move(edge: .trailing),
removal:.move(edge: .leading))
var transitionBack: AnyTransition = .asymmetric(
insertion: .move(edge: .leading),
removal:.move(edge: .trailing))
#State var transition: AnyTransition = .asymmetric(
insertion: .move(edge: .trailing),
removal:.move(edge: .leading))
var body: some View {
VStack {
HStack { // back and next step buttons
Button { // back button
print("go back")
withAnimation(.spring()) {
whichStep = 1
transition = transitionBack
}
} label: {
Image(systemName: "arrow.backward")
.font(.system(size: 20))
}
.padding()
Spacer()
Button { // next button
withAnimation(.spring()) {
whichStep = 2
transition = transitionNext
}
} label: {
Text("next step")
}
}
Spacer()
if whichStep == 1 {
ScrollView {
ForEach(0..<colors.count, id:\.self) { color in
withAnimation(.none) {
Circle()
.fill(colors[color])
.frame(width: 100, height: 100)
.matchedGeometryEffect(id: color, in: namespace)
.onTapGesture {
selectedColor = colors[color]
selection = color
}
}
}
}
Spacer()
} else if whichStep == 2 {
// withAnimation(.spring()) {
ThirdView(showScreen: $whichStep, namespace: namespace, selection: $selection, color: selectedColor)
.transition(transition)
// }
Spacer()
}
}
.padding()
}
}
SecondView:
struct ThirdView: View {
var namespace: Namespace.ID
#Binding var selection: Int
#State var color: Color
var body: some View {
VStack {
GeometryReader { geo in
Circle()
.fill(color)
.frame(width: 100, height: 100)
.matchedGeometryEffect(id: selection, in: namespace)
}
}
.ignoresSafeArea()
}
}
Related
I would like to have my cards expandable and fill the while area of the screen while they are doing the change form height 50 to the whole screen (and don't display the other components)
Here is my code:
import SwiftUI
struct DisciplineView: View {
var body: some View {
ScrollView(showsIndicators: false) {
LazyVStack {
Card(cardTitle: "Notes")
Card(cardTitle: "Planner")
Card(cardTitle: "Homeworks / Exams")
}
.ignoresSafeArea()
}
}
}
struct DisciplineV_Previews: PreviewProvider {
static var previews: some View {
DisciplineView()
}
}
import SwiftUI
struct Card: View {
#State var cardTitle = ""
#State private var isTapped = false
var body: some View {
RoundedRectangle(cornerRadius: 30, style: .continuous)
.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
.foregroundColor(.gray.opacity(0.2))
.frame(width: .infinity, height: isTapped ? .infinity : 50)
.background(
VStack {
cardInfo
if(isTapped) { Spacer() }
}
.padding(isTapped ? 10 : 0)
)
}
var cardInfo: some View {
HStack {
Text(cardTitle)
.font(.title).bold()
.foregroundColor(isTapped ? .white : .black)
.padding(.leading, 10)
Spacer()
Image(systemName: isTapped ? "arrowtriangle.up.square.fill" : "arrowtriangle.down.square.fill")
.padding(.trailing, 10)
.onTapGesture {
withAnimation {
isTapped.toggle()
}
}
}
}
}
struct Card_Previews: PreviewProvider {
static var previews: some View {
Card()
}
}
here is almost the same as I would like to have, but I would like the first one to be on the whole screen and stop the ScrollView while appearing.
Thank you!
Described above:
I would like to have my cards expandable and fill the while area of the screen while they are doing the change form height 50 to the whole screen (and don't display the other components)
I think this is pretty much what you are trying to achieve.
Basically, you have to scroll to the position of the recently presented view and disable the scroll. The scroll have to be disabled enough time to avoid continuing to the next item but at the same time, it have to be enabled soon enough to give the user the feeling that it is scrolling one item at once.
struct ContentView: View {
#State private var canScroll = true
#State private var itemInScreen = -1
var body: some View {
GeometryReader { geo in
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(0...10, id: \.self) { item in
Text("\(item)")
.onAppear {
withAnimation {
proxy.scrollTo(item)
canScroll = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
canScroll = true
}
}
}
}
.frame(width: geo.size.width, height: geo.size.height)
.background(Color.blue)
}
}
}
.disabled(!canScroll)
}
.ignoresSafeArea()
}
}
As the title says I am trying to create a grid of random numbers that must be clicked in ascending order and when pressed the background changes color so they're no longer visible.
I figured out how to create the grid from 0 to what ever size grid I want (5x5, 10x10, etc...)
I want the background to be white to start and then when the button is pressed it changes to black
The two biggest issues I'm having are all of the buttons turning black after I press on one button and the numbers randomize every time I press a button.
Any one have any ideas?
import SwiftUI
struct ContentView: View {
#State var buttonbackg = Color.white
#State var gridsize = 100
var body: some View {
//make grid
let cellCount = (gridsize/10)
let r = numbrand(min: 00, max: ((gridsize/10) * (gridsize/10) - 1))
ZStack{
Rectangle()
.ignoresSafeArea()
.background(Color.black)
VStack{
Text("Concentration Grid")
.font(.title)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.bottom)
Spacer()
}
VStack(spacing:3) {
Spacer()
ForEach(1...cellCount, id:\.self) { i in
HStack(spacing:3) {
Spacer()
ForEach(1...cellCount, id:\.self) { c in
let a = r()
ZStack {
Button(action:{
if (self.buttonbackg == .white) {
self.buttonbackg = .black
}
}){
Text("\(a)")
.foregroundColor(Color.black)
.frame(width: 28, height: 28)
.background(buttonbackg)
.cornerRadius(5)
.multilineTextAlignment(.center)
.scaledToFit()
.padding(.all, -2)
Spacer()
}
Spacer()
}
}
}
Spacer()
}
Spacer()
}
}
}
//^grid
func numbrand(min: Int, max:Int) -> () -> Int{
//let array: [Int] = Array(min...max)
var numbers: [Int] = []
return {
if numbers.isEmpty {
numbers = Array(min...max)
}
let index = Int(arc4random_uniform(UInt32(numbers.count)))
return numbers.remove(at: index)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Every-time you click a button, view redraws itself and thus your randomArray is created again
You need to create a Struct here which holds the property whether the
button is selected or not.
Give it a try this way:
struct concentrationGame: Hashable {
var id = UUID()
var number: Int = 0
var isSelected: Bool = false
}
class RandomNumbers: ObservableObject {
#Published var randomNumbers : [concentrationGame]
init() {
self.randomNumbers = (1...100).map{_ in arc4random_uniform(100)}.map({ concentrationGame(number: Int($0)) })
}
}
struct ContentView: View {
#ObservedObject var randomNumbers = RandomNumbers()
private var gridItemLayout = [GridItem(.adaptive(minimum: 50))]
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: gridItemLayout, spacing: 20) {
ForEach($randomNumbers.randomNumbers, id: \.self) { $randomNumbers in
Button(String(randomNumbers.number)) { randomNumbers.isSelected.toggle() }
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 50)
.foregroundColor(.white)
.background( randomNumbers.isSelected ? .white : .black)
.cornerRadius(10)
}
}
}
.padding()
.ignoresSafeArea(edges: .bottom)
.navigationTitle("Concentration Grid")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I have an overlay that displays when a toggle is turned to true. When it is displayed, it's supposed to have a transition from right to left of the view for a nice transition between the views. The issue is the animation animates from the top right corner and shoots down, in a diagonal motion.
I'm assuming either the animation is incorrect or theres something throwing off the animation of the overlay.
Thank you in advance for your help.
struct createUserAccount: View {
let transition: AnyTransition = .asymmetric(insertion: .move(edge:.trailing), removal: .move(edge: .leading))
//Stores form info as userInfo, stored in userModel
#ObservedObject var userInformation = FormViewModel()
#ObservedObject var keyboardResponder = KeyboardResponder()
#State private var showFitnessForm = false
#Environment (\.managedObjectContext) var moc //calls managed Object context (datacore)
#Environment (\.dismiss) var dismiss
#FetchRequest(sortDescriptors: []) var allUsers: FetchedResults <User>
init(){
UITableView.appearance().backgroundColor = .clear
}
var body: some View {
ZStack{
Form{
Section(){
TextField("FirstName", text: $userInformation.firstname)
TextField("LastName", text: $userInformation.lastname)
TextField("Email", text: $userInformation.email)
if userInformation.emailPrompt != "" {
Text(userInformation.emailPrompt)
.font(.caption).italic().foregroundColor(.red)
}
SecureField("Passsword", text: $userInformation.password
).autocapitalization(.none)
if userInformation.passwordPrompt != "" {
Text(userInformation.passwordPrompt)
.font(.caption).italic().foregroundColor(.red)
}
}
.listRowBackground(Color.clear)
.padding()
.navigationBarTitle(Text("Lets Get Started"))
.navigationBarTitleDisplayMode(.automatic)
.font(.system(size:18))
.listStyle(GroupedListStyle())
}
Button("Continue"){
let newUser = User(context: moc)
newUser.id = UUID()
newUser.firstName = userInformation.firstname
newUser.lastName = userInformation.lastname
newUser.email = userInformation.email
newUser.password = userInformation.password
showFitnessForm.toggle()
// dismiss()
}
.frame(width: 150, height: 20)
.foregroundColor(.white)
.padding()
.background(LinearGradient(gradient: Gradient(colors: [.orange, .pink]), startPoint: .leading, endPoint: .bottom))
.font(.title3)
.background(.clear)
.cornerRadius(5)
.offset(y:30)
.opacity(userInformation.isSignUpComplete ? 1 : 0.5)
.offset(y: keyboardResponder.currentHeight*2)
.disabled(!userInformation.isSignUpComplete)
.frame(height: 300)
HStack{
Text("Already a User?").italic().font(.callout)
NavigationLink(destination: userLogin() .navigationBarHidden(true)){
Text("Login")
.foregroundColor(.pink).font(.callout)
}
}
.offset(y:80)
.offset(y: keyboardResponder.currentHeight*2)
}
.overlay(
showFitnessForm ? FitnessForm(
userFirstName: $userInformation.firstname,
userLastName: $userInformation.lastname,
userEmailAddress: $userInformation.email,
userLoginPassword: $userInformation.password
) .animation(Animation.easeInOut(duration: 1.0), value: offset)
.transition(transition) : nil )
}
}
Try this
.overlay(
VStack {
if showFitnessForm {
FitnessForm(...)
.transition(transition) // << here !!
}
}
.animation(Animation.easeInOut(duration: 1.0), value: showFitnessForm) // << here !!
)
*about diagoanal movement - I assume your view is in NavigationView, so look at this one Broken animation in NavigationView
When I scroll from one page to the other the horizontal indicator bar doesn't move, what would be the appropriate way to move it (with animation if possible)?
The green area below doesn't move to the right once I switch to the analytics area.
Here's the full code:
enum PortfolioTabBarOptions: Hashable, CaseIterable {
case balance, analytics
var menuString: String { String(describing: self) }
}
struct CustomPagerTabView: View {
#Binding var selectedTab: PortfolioTabBarOptions
#State var tabOffset: CGFloat = 0
var body: some View {
VStack {
HStack(alignment: .center) {
HStack(spacing: 100) {
ForEach(Array(PortfolioTabBarOptions.allCases.enumerated()), id: \.element) { index, element in
// Current Tab
if(selectedTab.menuString == element.menuString) {
Text(element.menuString.capitalizeFirstLetter())
.font(.system(size: 15.0))
.bold()
.onTapGesture() {
selectedTab = element.self
}
.onAppear {
self.tabOffset = CGFloat(index)
}
}
// Innactive Tabs
else {
Text(element.menuString.capitalizeFirstLetter())
.foregroundColor(.gray)
.font(.system(size: 15.0))
.bold()
.onTapGesture() {
selectedTab = element.self
}
}
}
}
.padding(.horizontal)
}
// Indicator...
Capsule()
.fill(.gray)
// Width of the indicator bar
.frame(width: PortfolioTabBarOptions.allCases.count == 0 ? 0 : (getScreenBounds().width / CGFloat(PortfolioTabBarOptions.allCases.count)), height: 1.2)
.padding(.top,10)
.frame(maxWidth: .infinity,alignment: .leading)
.offset(x: tabOffset)
}
.padding(.top)
}
}
The green area below does move to the right, but only 1 pixel. Try something like this example code, choose the value (200) most suited for your purpose:
.onAppear {
self.tabOffset = CGFloat(index*200) // <-- here
}
Question is simple: how in the world do i get a Text to animate properly?
struct ContentView: View {
#State var foozle: String = ""
var body: some View {
VStack() {
Spacer()
Text(self.foozle)
.frame(maxWidth: .infinity)
.transition(.opacity)
Button(action: {
withAnimation(.easeInOut(duration: 2)) {
self.foozle = "uuuuuuuuu"
}
}) { Text("ugh") }
Spacer()
}.frame(width: 320, height: 240)
}
}
The problem: the view insists on doing some dumb animation where the text is replaced with the new text, but truncated with ellipses, and it slowly expands widthwise until the entirety of the new text is shown.
Naturally, this is not an animation on opacity. It's not a frame width problem, as I've verified with drawing the borders.
Is this just another dumb bug in SwiftUI that i'm going to have to deal with, and pray that someone fixes it?
EDIT: ok, so thanks to #Mac3n, i got this inspiration, which works correctly, even if it's a little ugly:
Text(self.foozle)
.frame(maxWidth: .infinity)
.opacity(op)
Button(action: {
withAnimation(.easeOut(duration: 0.3)) {
self.op = 0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.foozle += "omo"
withAnimation(.easeIn(duration: 0.3)) {
self.op = 1
}
}
}
}) {
Text("ugh")
}
The problem is that SwiftUI sees Text view as the same view. You can use the .id() method on the view to set it. In this case I've just set the value to a hash of the text itself, so if you change the text, the entire view will get replaced.
struct ContentView: View {
#State var foozle: String = ""
var body: some View {
VStack() {
Spacer()
Text(self.foozle)
.id(self.foozle.hashValue)
.frame(maxWidth: .infinity)
.transition(.opacity)
Button(action: {
withAnimation(.easeInOut(duration: 2)) {
self.foozle = "uuuuuuuuu"
}
}) { Text("ugh") }
Spacer()
}.frame(width: 320, height: 240)
}
}
Transition works when view appeared/disappeared. In your use-case there is no such workflow.
Here is a demo of possible approach to hide/unhide text with opacity animation:
struct DemoTextOpacity: View {
var foozle: String = "uuuuuuuuu"
#State private var hidden = true
var body: some View {
VStack() {
Spacer()
Text(self.foozle)
.frame(maxWidth: .infinity)
.opacity(hidden ? 0 : 1)
Button(action: {
withAnimation(.easeInOut(duration: 2)) {
self.hidden.toggle()
}
}) { Text("ugh") }
Spacer()
}.frame(width: 320, height: 240)
}
}
If you want to animate on opacity you need to change opacity value on your text element.
code example:
#State private var textValue: String = "Sample Data"
#State private var opacity: Double = 1
var body: some View {
VStack{
Text("\(textValue)")
.opacity(opacity)
Button("Next") {
withAnimation(.easeInOut(duration: 0.5), {
self.opacity = 0
})
self.textValue = "uuuuuuuuuuuuuuu"
withAnimation(.easeInOut(duration: 1), {
self.opacity = 1
})
}
}
}