SwiftUI Animation: How to Move THEN Show? - swiftui

Example
Here is a menu with three items:
This is fine. But what I would like to achieve is:
Menu expands
Then the three images fade-in AFTER the menu is done expanding
I thought maybe adding a 2nd, delayed animation for the opacity might work but instead, it looks like all animation (movement and opacity) gets delayed:
Here is the code:
struct SequenceAnimation_SOQuestion: View {
#State private var show = false
var body: some View {
HStack(spacing: 40) {
Group {
Image(systemName: "pencil")
Image(systemName: "scribble")
Image(systemName: "lasso")
}
.opacity(show ? 1 : 0)
.animation(Animation.default.delay(0.5))
Button(action: { self.show.toggle() }) {
Image(systemName: "line.horizontal.3.decrease")
.rotationEffect(.degrees(-90))
}.offset(x: 10)
}
.padding(20)
.padding(.leading, 40)
.foregroundColor(.white)
.background(Capsule().fill(Color.blue))
.font(.largeTitle)
.offset(x: show ? -70 : -320)
.animation(.default)
}
}

You should add each animation separately, then you can have different animations for each one:
struct ContentView: View {
#State private var isMenuCollapsed = true
#State private var isItemsVisible = false
var body: some View {
HStack(spacing: 40) {
Group {
Image(systemName: "pencil")
Image(systemName: "scribble")
Image(systemName: "lasso")
}
.opacity(isItemsVisible ? 1 : 0)
Button(action: {
withAnimation(Animation.default) {
self.isMenuCollapsed.toggle()
}
withAnimation(Animation.default.delay(0.2)) {
self.isItemsVisible.toggle()
}
}) {
Image(systemName: "line.horizontal.3.decrease")
.rotationEffect(.degrees(-90))
}.offset(x: 10)
}
.padding(20)
.padding(.leading, 40)
.foregroundColor(.white)
.background(Capsule().fill(Color.blue))
.font(.largeTitle)
.offset(x: isMenuCollapsed ? -320 : -70)
}
}

I don't know exactly what kind of animation you want, but i worked something out which may be somewhere around your goal.
I changed the working of the animations. In your 2nd example your images were animation not right so that's what the following line fixes:
struct SequenceAnimation_SOQuestion: View {
#State private var show = false
var body: some View {
HStack(spacing: 40) {
Group {
Image(systemName: "pencil")
Image(systemName: "scribble")
Image(systemName: "lasso")
}
.opacity(show ? 1 : 0)
.animation(!self.show ? .default : Animation.default.delay(0.5))
Button(action: { self.show.toggle() }) {
Image(systemName: "line.horizontal.3.decrease")
.rotationEffect(.degrees(-90))
}.offset(x: 10)
}
.padding(20)
.padding(.leading, 40)
.foregroundColor(.white)
.background(Capsule().fill(Color.blue))
.font(.largeTitle)
.offset(x: show ? -70 : -320)
.animation(!self.show ? Animation.default.delay(0.5) : .default)
}
}
If you provide me a bit more specific information about your goal you're trying to achieve, I can maybe help you.

Related

SwiftUI: Show specific views with an animation delay

I'm trying to achieve the following animation: When I tap on a rectangle the rectangle should be expanded to the full width with an close button in the corner and below this rectangle a ScrollView should appear. This works so far without any problems. Now I would like to display the ScrollView a little bit later then the expanded rectangle. So when I tap on the rectangle: First the expanded rectangle with the close button should appear and 3 seconds later the ScrollView.
struct Playground: View {
#Namespace var namespace
#State var show = false
private let gridItems = [GridItem(.flexible())]
var body: some View {
if show {
VStack{
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)){
Rectangle()
.matchedGeometryEffect(id: "A", in: namespace, isSource: show)
.frame(height: 300)
.frame(maxWidth: .infinity)
Image(systemName: "xmark")
.font(.system(size: 25))
.foregroundColor(.white)
.background(Color.red)
.padding(20)
.onTapGesture {
withAnimation(.spring()){
self.show = false
}
}
}
// SHOW THIS SCROLLVIEW 3 SECONDS LATER
ScrollView{
LazyVGrid(columns: gridItems){
ForEach(0..<10){ cell in
Text("\(cell)")
}
}
}
.animation(Animation.spring().delay(3)) // doesn't work!
}
} else {
Rectangle()
.matchedGeometryEffect(id: "A", in: namespace, isSource: !show)
.frame(width: 100, height: 100)
.onTapGesture {
withAnimation(.spring()){
self.show = true
}
}
}
}
}
We need to make separated animation (and related state) for ScrollView in this scenario.
Here is possible approach. Tested with Xcode 12.1 / iOS 14.1
struct Playground: View {
#Namespace var namespace
#State var show = false
private let gridItems = [GridItem(.flexible())]
#State private var showItems = false
var body: some View {
if show {
VStack{
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)){
Rectangle()
.matchedGeometryEffect(id: "A", in: namespace, isSource: show)
.frame(height: 300)
.frame(maxWidth: .infinity)
Image(systemName: "xmark")
.font(.system(size: 25))
.foregroundColor(.white)
.background(Color.red)
.padding(20)
.onTapGesture {
withAnimation(.spring()){
self.show = false
}
}
}
VStack {
if showItems {
ScrollView{
LazyVGrid(columns: gridItems){
ForEach(0..<10){ cell in
Text("\(cell)")
}
}
}
} else {
Spacer()
}
}
.onAppear { showItems = true }
.onDisappear { showItems = false }
.animation(Animation.spring().delay(3), value: showItems)
}
} else {
Rectangle()
.matchedGeometryEffect(id: "A", in: namespace, isSource: !show)
.frame(width: 100, height: 100)
.onTapGesture {
withAnimation(.spring()){
self.show = true
}
}
}
}
}

How Can I Handle Tap Gesture on Widget?

I'm writing a widget with WidgetKit and I want to make the widget's content is clickable. For example, if users click to standings, I want to open the standings tab when the app becomes active.
I tried to use notification between the app and widget but the tap gesture is not working, I added print inside of the tap gesture but it did not appear in the console. Also, I added the same app group to both of them.
WidgetView:
struct LargeWidget : View {
#State var standings : [StandingsTable]
var body: some View {
VStack(alignment:.leading){
if standings.count > 0{
HStack(spacing:5){
Text("#").foregroundColor(.gray)
.frame(width: 30)
Text("Team".localized).foregroundColor(.gray)
Spacer()
Text("_D".localized).foregroundColor(.gray)
.frame(width: 30)
Text("_L".localized).foregroundColor(.gray)
.frame(width: 30)
Text("_W".localized).foregroundColor(.gray)
.frame(width: 30)
Text("_P".localized).foregroundColor(.gray)
.frame(width: 30)
}
Divider()
ForEach(0..<5, id: \.self) { i in
HStack(spacing:5){
Text(standings[i].rank)
.font(.system(size: 15))
.padding(.vertical, 3)
.frame(width: 30)
.background(Color(UIColor.systemBackground))
.cornerRadius(4)
Text(standings[i].name)
.lineLimit(1)
.padding(.leading, 5)
Spacer()
Text(standings[i].drawn)
.frame(width: 30)
Text(standings[i].lost)
.frame(width: 30)
Text(standings[i].won)
.frame(width: 30)
Text(standings[i].points)
.frame(width: 30)
}
.padding(.vertical, 5)
.background(standings[i].name == "Besiktas" ? Color(UIColor.systemGray6) : Color.clear)
.cornerRadius(8)
}
Spacer(minLength: 0)
}else{
Text("Large")
.padding()
}
}.padding()
.onTapGesture {
print("clicked to standings")
DispatchQueue.main.async(execute: {
NotificationCenter.default.post(name: NSNotification.Name("standings"), object: nil, userInfo: nil)
})
}
}
}
and here ContentView in app:
import SwiftUI
extension NSNotification {
static let openStandings = NSNotification.Name.init("standings")
}
struct ContentView: View {
#State var show: Bool = false
var body: some View {
NavigationView{
Text("Hello, world!")
.padding()
}.sheet(isPresented: self.$show) {
VStack{
Text("Notification")
.padding()
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.openStandings))
{ obj in
self.show.toggle()
}
}
}
Screenshot of Widget
Okay I created a new project with SwiftUI Environments (not old way AppDelegate and SceneDelegate).
Then I use Link for tap actions and I got it with .onOpenURL modifier in ContentView. It works :)
ContentView:
import SwiftUI
enum SelectedTab: Hashable {
case home
case standings
case options
}
struct ContentView: View {
#State var selectedTab: SelectedTab = .home
var body: some View {
TabView(selection: self.$selectedTab) {
Text("Home")
.tabItem {
Image(systemName: "house")
.renderingMode(.template)
Text("Home")
}.tag(SelectedTab.home)
Text("Standings")
.tabItem {
Image(systemName: "list.number")
.renderingMode(.template)
Text("Standings")
}.tag(SelectedTab.standings)
Text("Options")
.tabItem {
Image(systemName: "gear")
.renderingMode(.template)
Text("Options")
}.tag(SelectedTab.options)
.onOpenURL { (url) in
if url.absoluteString == "widget-deeplink://standings"{
self.selectedTab = .standings
}
}
}
}
}
Link usage example in Widget:
Link(destination: URL(string: "widget-deeplink://standings")!) {
Text("Link Test")
}

How to use UserDefault on an Array in SwiftUI

I'm working on a checklist app that has several arrays with checks. I'd like to save the state if a users closes/quits the app. I was thinking of using the UserDefault methods for this:
HStack {
ForEach(0 ..< checklist.steps) { index in
VStack {
Button(action: {
self.checked[index].toggle()
UserDefaults.standard.set(self.checked[index], forKey: "Check")
I'm currently using the following state for checks:
#State private var checked = [false, false, false, false, false, false]
Does anyone know how to apply UserDefaults for arrays or generally how to save the state for your app when closing it?
Thanks in advance!
try this: (it is the code from last time ;)) i think the true/false settings itself is not correct, but the saving / loading works ;)
struct ChecklistView: View {
var checklist: Checklist
#State var currentProgress: Float = 0.0
#State var checked : [Bool] = [false, false, false, false]
init(checklist: Checklist) {
self.checklist = checklist
}
func saveUserDefaults() {
var value = ""
for eachValue in checked {
if eachValue == true { value += "1" }
else { value += "0" }
}
UserDefaults.standard.set(value, forKey: checklist.title)
}
var body: some View {
ZStack(alignment: .leading) {
// RoundedRectangle(cornerRadius: 20)
// .foregroundColor(.red).opacity(0.5)
// .frame(width: 200)
VStack {
HStack(alignment: .top) {
checklist.image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
.padding(.trailing)
VStack(alignment: .leading) {
Text(checklist.title)
.font(.system(size: 16, weight: .medium))
.padding(.bottom, 4)
Text(checklist.instruction.uppercased()).font(.system(size: 12))
HStack {
ForEach(0 ..< checklist.steps) { index in
VStack {
Button(action: {
self.checked[index].toggle()
print(self.checked)
self.saveUserDefaults()
}) {
ZStack {
RoundedRectangle(cornerRadius: 8)
.foregroundColor(self.checked[index] ? Color("LightGreen") : .gray )
.frame(width: 40, height: 40)
Image(systemName: self.checked[index] ? "checkmark" : "plus")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
.foregroundColor(self.checked[index] ? .white : .white)
}
}
}
}
}
}
}.frame(width: 350, alignment: .leading)
}
}
.onAppear() {
if let values = UserDefaults.standard.value(forKey: self.checklist.title) as? String {
self.checked = values.map {
if $0 == "1" { return true }
return false
}
}
print(self.checked)
}
.frame(width: 350)
.padding(.bottom, 16)
.cornerRadius(8)
.padding(.top, 20)
}
}
I haven't test that solution, but can't you store your array in your NSUserdefaults after every button action?
Button(action: {
self.checked[index].toggle()
UserDefaults.standard.set(self.checked, forKey: "Check")
Probably, the best solution would be using CoreData for that, which would be very easier to use.
PS: I can test that solution later on.. not at my Mac right now

Horizontal scrollview is not working swiftUI

i'm trying to show some horizontal view but it's not working. below are the code i'm using.
#State var userDataList = [UserModel]()
var body: some View{
VStack(spacing: 10){
VStack{
prefView()
.padding(.bottom, 30)
Text("Tap to start making friend")
}
ScrollView(.horizontal, content: {
HStack(spacing: 10) {
ForEach(userDataList) { user in
NavigationLink(destination: ChatView(chatMember: self.chatmember, user: user)){
SearchUser()
}
}
}
.padding(.leading, 10)
})
.frame(width: 300, height: 400)
Spacer()
}
.navigationBarTitle("Profile",displayMode: .inline)
.onAppear {
self.userList()
}
}
But if i change the above code with below than it work i don't know how please help me.
ScrollView(.horizontal, content: {
HStack(spacing: 10) {
ForEach(0..< 2) { index in
NavigationLink(destination: ChatView(chatMember: self.chatmember, user: user)){
SearchUser()
}
}
}
.padding(.leading, 10)
})
Thanks in advance

SwiftUI TextField acts disabled in VStack inside ZStack (simulating an Alert with a TextField)

I need to make an Alert in SwiftUI that has an editable TextField in it. Currently, this isn't supported by SwiftUI (as of Xcode 11.3), so I'm looking for a work-around.
I know I can implement by wrapping the normal UIKit bits in a UIHostingController, but really want to stick with an all-SwiftUI implementation.
I've got two VStacks in a ZStack, with the front one (the one with the TextView) being hidden and disabled until until you tap the button. Take a look at this:
import SwiftUI
struct ContentView: View {
#State var isShowingEditField = false
#State var text: String = "12345"
var body: some View {
ZStack {
VStack {
Text("Value is \(self.text)")
Button(action: {
print("button")
self.isShowingEditField = true
}) {
Text("Tap To Test")
}
}
.disabled(self.isShowingEditField)
.opacity(self.isShowingEditField ? 0.25 : 1.00)
VStack(alignment: .center) {
Text("Edit the text")
TextField("", text: self.$text)
.multilineTextAlignment(.center)
.lineLimit(1)
Divider()
HStack {
Button(action: {
withAnimation {
self.isShowingEditField = false
print("completed... value is \(self.text)")
}
}) {
Text("OK")
}
}
}
.padding()
.background(Color.white)
.shadow(radius: CGFloat(1.0))
.disabled(!self.isShowingEditField)
.opacity(self.isShowingEditField ? 1.0 : 0.0)
}
}
}
This seems like it should work to me. Switching between the two VStacks works well, but the TextField is not editable.
It acts like it's disabled, but it's not. Explicitly .disabled(false) to the TextField doesn't help. Also, it should already be enabled anyway since 1) that's the default, 2) the VStack it's in is specifically being set as enabled, and 3) The OK button works normally.
Ideas/workarounds?
Thanks!
You need to force the TextField updating with some methods. Like the following:
TextField("", text: self.$text)
.multilineTextAlignment(.center)
.lineLimit(1)
.id(self.isShowingEditField)
That is really ridiculous, but the problem disappears if you comment just one line of code:
//.shadow(radius: CGFloat(1.0))
I commented it and everything works. I think you need to use ZStack somewhere in your custom alert view to avoid this.. hm, bug, maybe?
update
try some experiments. If you leave just this code in that View:
struct CustomAlertWithTextField: View {
#State var isShowingEditField = false
#State var text: String = "12345"
var body: some View {
TextField("", text: $text)
.padding()
.shadow(radius: CGFloat(1.0))
}
}
it would not work again. only if you comment .padding() or .shadow(...)
BUT if you relaunch Xcode - this code begin to work (which made me crazy). Tried it at Xcode Version 11.2 (11B52)
update 2 the working code version:
struct CustomAlertWithTextField: View {
#State var isShowingEditField = false
#State var text: String = "12345"
var body: some View {
ZStack {
VStack {
Text("Value is \(self.text)")
Button(action: {
print("button")
self.isShowingEditField = true
}) {
Text("Tap To Test")
}
}
.disabled(self.isShowingEditField)
.opacity(self.isShowingEditField ? 0.25 : 1.00)
ZStack {
Rectangle()
.fill(Color.white)
.frame(width: 300, height: 100)
.shadow(radius: 1) // moved shadow here, so it doesn't affect TextField now
VStack(alignment: .center) {
Text("Edit the text")
TextField("", text: self.$text)
.multilineTextAlignment(.center)
.lineLimit(1)
.frame(width: 298)
Divider().frame(width: 300)
HStack {
Button(action: {
withAnimation {
self.isShowingEditField = false
print("completed... value is \(self.text)")
}
}) {
Text("OK")
}
}
}
}
.padding()
.background(Color.white)
.disabled(!self.isShowingEditField)
.opacity(self.isShowingEditField ? 1.0 : 0.0)
}
}
}
after some research i solved this problem by adding .clipped() to the VStack.
For your problem, it would look like this:
VStack(alignment: .center) {
Text("Edit the text")
TextField("", text: self.$text)
.multilineTextAlignment(.center)
.lineLimit(1)
Divider()
HStack {
Button(action: {
withAnimation {
self.isShowingEditField = false
print("completed... value is \(self.text)")
}
}) {
Text("OK")
}
}
}
.padding()
.clipped() // <- here
.background(Color.white)
.shadow(radius: CGFloat(1.0))
.disabled(!self.isShowingEditField)
.opacity(self.isShowingEditField ? 1.0 : 0.0)