How can I use the same randomly generated number twice? - SwiftUI - random

I'm trying to build a very simple multiplication practice app where the user picks the multiplication table they want to practice and the app poses multiplication problems on that table using a randomly generated integer between 1 and 20. I then want to give the user the answer.
The problem that I'm having is that the randomly generated number changes between my code for showing the question and my code for showing the answer. I'm sure there's a simple solution to this, but I'm not seeing it.
How do I store a randomly generated number for use in two places in my code? Alternatively, how do I selectively stop the randomization from happening?
Here's my current code:
struct test: View {
#State private var multiplicationTable = 1
var body: some View {
VStack {
Spacer()
Text("Pick the multiplication table you'd like to practice!")
.bold()
.fontWeight(.bold)
.font(.title)
.multilineTextAlignment(.center)
Stepper(value: $multiplicationTable, in: 1...12, step: 1) {
Text("\(multiplicationTable)'s")
}
Text("Let's go!")
Spacer()
Text("Question #1: \(numberForText(multiplier1: multiplicationTable).text1)")
Text("Answer: \(numberForText(multiplier1: multiplicationTable).text2)")
Spacer()
}
.padding(.leading, 20)
.padding(.trailing, 20)
}
func numberForText(multiplier1: Int) -> (text1: String, text2: String) {
let multiplier2 = Int.random(in: 1..<21)
return ("What is \(multiplier1) times \(multiplier2)?", "\(multiplier1) times \(multiplier2) = \(multiplier1 * multiplier2)")
}
}
Thank you.

You will need to get data from your numberForText method just once. And store the returned tuple to a property. That's it!
struct test: View {
#State private var multiplicationTable = 1
#State private var generatedTuple: (text1: String, text2: String) = ("", "")
var body: some View {
VStack {
Spacer()
Text("Pick the multiplication table you'd like to practice!")
.bold()
.fontWeight(.bold)
.font(.title)
.multilineTextAlignment(.center)
Stepper(value: $multiplicationTable, in: 1...12, step: 1, onEditingChanged: { (didChange) in
self.generatedTuple = self.numberForText(multiplier1: self.multiplicationTable)
}) {
Text("\(multiplicationTable)'s")
}
Text("Let's go!")
Spacer()
Text("Question #1: \(self.generatedTuple.text1)")
Text("Answer: \(self.generatedTuple.text2)")
Spacer()
}
.padding(.leading, 20)
.padding(.trailing, 20)
}
func numberForText(multiplier1: Int) -> (text1: String, text2: String) {
let multiplier2 = Int.random(in: 1..<21)
return ("What is \(multiplier1) times \(multiplier2)?", "\(multiplier1) times \(multiplier2) = \(multiplier1 * multiplier2)")
}
}
As you can see, I make use of Stepper's onEditingChanged closure event. Inside that event, I call on your numberForText method, and the returned tuple is stored in the property:
#State private var generatedTuple: (text1: String, text2: String) = ("", "").
And the Text values for Question and Answer will be the data from the stored tuple - and you already know how to extract them.

You just need to store generated pair question-answer. Please find below possible approach:
struct test: View {
#State private var multiplicationTable = 1
#State private var excesize: (text1: String, text2: String) = ("", "")
var body: some View {
VStack {
Spacer()
Text("Pick the multiplication table you'd like to practice!")
.bold()
.fontWeight(.bold)
.font(.title)
.multilineTextAlignment(.center)
Stepper(value: $multiplicationTable, in: 1...12, step: 1) {
Text("\(multiplicationTable)'s")
}
Text("Let's go!")
Spacer()
Text("Question #1: \(excesize.text1)")
Text("Answer: \(excesize.text2)")
Spacer()
Button("Next turn") { self.generateNext() }
}
.padding(.leading, 20)
.padding(.trailing, 20)
}
func generateNext() {
self.excesize = numberForText(multiplier1: multiplicationTable)
}
func numberForText(multiplier1: Int) -> (text1: String, text2: String) {
let multiplier2 = Int.random(in: 1..<21)
return ("What is \(multiplier1) times \(multiplier2)?", "\(multiplier1) times \(multiplier2) = \(multiplier1 * multiplier2)")
}
}

Related

Dealing cards from a deck - MatchedGeometryEffect - Multiple Inserted Views

I'm trying to deal groups of cards from a deck, and have the cards fly from the deck to their respective spots on screen (Stanford 2021 IOS class SET game). I'm tracking the status of each card, including whether it has been dealt or not. I thought I had all the logic correct, but the animation using a matchedGeometryEffect is not working. Both in the canvas view and when running the application in the simulator, the cards just go to their final spots in the overall View, and do not animate individually.
In an earlier version, I was seeing multiple debugger messages about "Multiple inserted views in matched geometry group Pair<Int, ID> have 'isSource: true'", but those messages no longer appear. With this slimmed-down version, the cards simply fail to animate between their original positions (in the card deck) and their final positions in the VGrid.
This codes uses a modified LazyVGrid to display the cards, with a control called AspectVGrid. The primary difference between the original LazyVGrid and the AspectVGrid is that the AspectVGrid creates a LazyVGrid with a fixed aspect ratio. The AspectVGrid only controls the grid layout itself. The code for that struct is included below.
When the application starts, all the cards are available in the deck, and each card's view is assigned its source matchedGeometryEffect. The cards are all marked as undealt (in a #State Set of card IDs). When the deck is tapped (onTapGesture), the model updates either twelve or three additional cards as wasDealt, and those newly dealt cards are supposed to be animated via a "withAnimation" code block, with individual Views in the AspectVGrid as their destinations.
Any suggestions on how to resolve this would be welcome. This seems like a pretty straightforward process, but I am clearly missing something in my implementation.
Thanks in advance for any ideas on this. I've included the model, View Model, and Views below.
Model
import Foundation
import SwiftUI
struct dataModel {
struct Card: Identifiable {
var wasDealt: Bool
var wasDiscarded: Bool
var dealDelay: Double
let id: Int
}
private(set) var firstCardWasDealt: Bool = false
private(set) var cards: Array<Card>
var cardsDealt: Array<Card> {
get {cards.filter({ card in card.wasDealt})}
}
private var numberOfCardsInPlay: Int {
get { cardsDealt.count }
}
var cardsDisplayed: Array<Card> {
get {cardsDealt.filter({ card in !card.wasDiscarded })}
}
var cardsInDeck: Array<Card> {
get {cards.filter({ card in !card.wasDealt })}
}
init() {
cards = []
newGame()
}
// Divides the total time to deal the cards by the number of cards to deal,
// and provides the applicable delay to the nth (index) card in the group of
// cards to be dealt.
private func calcDelay(numCards: Int, index: Int, totalDelay: Double) -> Double {
return Double(index) * (totalDelay / Double(numCards))
}
// If no cards have been dealt, deal twelve cards. Otherwise, deal three cards.
// When dealing the cards, apply the appropriate delay to each card being dealt.
// If all cards are already in play, don't deal any more cards.
mutating func dealMoreCards() {
if !firstCardWasDealt {
for index in (0...11) {
cards[index].dealDelay = calcDelay(numCards: 12, index: index, totalDelay: CardConstants.total12CardDealDuration)
cards[index].wasDealt = true
}
firstCardWasDealt = true
} else {
if numberOfCardsInPlay < cards.count {
let startIndex = numberOfCardsInPlay
for index in (startIndex...(startIndex + 2)) {
cards[index].dealDelay = calcDelay(numCards: 3, index: index, totalDelay: CardConstants.total3CardDealDuration)
cards[index].wasDealt = true
}
}
}
}
mutating func newGame() {
firstCardWasDealt = false
cards = []
for index in (0...80) {
cards.append(Card(wasDealt: false, wasDiscarded: false, dealDelay: 0, id: index))
}
}
}
struct CardConstants {
static let color = Color.red
static let aspectRatio: CGFloat = 2/3
static let dealAnimationDuration: Double = 0.2 // 0.5 - This value controls how long it takes to animate each card
static let total12CardDealDuration: Double = 3.0 // this controls how long it takes to deal twelve cards (in seconds)
static let total3CardDealDuration: Double = 0.75 // this controls how long it takes to deal three cards (in seconds)
}
View Model
import SwiftUI
class ViewModel: ObservableObject {
#Published private var model: dataModel
init() {
model = dataModel()
}
var cardsDealt: Array<dataModel.Card> {
model.cardsDealt
}
var cardsDisplayed: Array<dataModel.Card> {
model.cardsDisplayed
}
var cardsInDeck: Array<dataModel.Card> {
model.cardsInDeck
}
func choose(_ card: dataModel.Card) {
// do something with the chosen card
}
func dealMoreCards() {
model.dealMoreCards()
}
}
View
import SwiftUI
struct exampleView: View {
#ObservedObject var example: ViewModel
#Namespace private var dealingNamespace
#State private var dealt = Set<Int>()
private func deal(_ card: dataModel.Card) {
dealt.insert(card.id)
}
private func isUnDealt(_ card: dataModel.Card) -> Bool {
!dealt.contains(card.id)
}
var body: some View {
VStack {
Text("Example - \(example.cardsDisplayed.count) cards Displayed")
AspectVGrid(items: example.cardsDisplayed, aspectRatio: 2/3, content: { card in CardView(card: card)
.padding(4)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
.transition(AnyTransition.asymmetric(insertion: .opacity, removal: .opacity))
.onTapGesture {
example.choose(card)
}
})
HStack {
deckBody
Spacer()
}
}
}
var deckBody: some View {
ZStack {
ForEach(example.cardsInDeck) {
card in CardView(card: card)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
.transition(AnyTransition.asymmetric(insertion: .opacity, removal: .identity))
}
}
.frame(width: 60, height: 90)
.foregroundColor(.cyan)
.onTapGesture {
example.dealMoreCards() // Make the next group of cards available (12 or 3)
// deal cards
for card in example.cardsDealt {
if isUnDealt(card) {
withAnimation(Animation.easeInOut(duration: CardConstants.dealAnimationDuration).delay(card.dealDelay)) {
deal(card)
}
}
}
}
}
}
struct CardView: View {
let card: dataModel.Card
var body: some View {
VStack {
ZStack {
let shape = RoundedRectangle(cornerRadius: 5)
if card.wasDealt {
shape.fill().foregroundColor(.white)
} else {
shape.fill().foregroundColor(.cyan)
}
shape.strokeBorder(lineWidth: 3)
Text("Card: \(card.id)")
.padding(4)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let example = ViewModel()
exampleView(example: example)
}
}
AspectVGrid
//
// AspectVGrid.swift
//
// Created by CS193p Instructor on 4/14/21.
// Copyright Stanford University 2021
//
import SwiftUI
struct AspectVGrid<Item, ItemView>: View where ItemView: View, Item: Identifiable {
var items: [Item]
var aspectRatio: CGFloat
var content: (Item) -> ItemView
init(items: [Item], aspectRatio: CGFloat, #ViewBuilder content: #escaping (Item) -> ItemView) {
self.items = items
self.aspectRatio = aspectRatio
self.content = content
}
var body: some View {
GeometryReader { geometry in
VStack {
let width: CGFloat = widthThatFits(itemCount: items.count, in: geometry.size, itemAspectRatio: aspectRatio)
LazyVGrid(columns: [adaptiveGridItem(width: width)], spacing: 0) {
ForEach(items) { item in
content(item).aspectRatio(aspectRatio, contentMode: .fit)
}
}
Spacer(minLength: 0)
}
}
}
private func adaptiveGridItem(width: CGFloat) -> GridItem {
var gridItem = GridItem(.adaptive(minimum: width))
gridItem.spacing = 0
return gridItem
}
private func widthThatFits(itemCount: Int, in size: CGSize, itemAspectRatio: CGFloat) -> CGFloat {
var columnCount = 1
var rowCount = itemCount
repeat {
let itemWidth = size.width / CGFloat(columnCount)
let itemHeight = itemWidth / itemAspectRatio
if CGFloat(rowCount) * itemHeight < size.height {
break
}
columnCount += 1
rowCount = (itemCount + (columnCount - 1)) / columnCount
} while columnCount < itemCount
if columnCount > itemCount {
columnCount = itemCount
}
return floor(size.width / CGFloat(columnCount))
}
}
MatchedGeometryEffectApp
import SwiftUI
#main
struct MatchedGeometryEffectApp: App {
private let example = ViewModel()
var body: some Scene {
WindowGroup {
exampleView(example: example)
}
}
}
So, the problem you have been having with the matched geometry seems to be that AspectVGrid, which you were given as part of the assignment, interferes with the matched geometry animation. I suspect it has to do with the resizing mechanism. Removing that gives you a matched geometry that does what one would expect.
Here is a working example of the matched geometry in your ExampleView. I also rewrote CardView to make it simpler.
struct ExampleView: View {
#ObservedObject var example: ViewModel
#Namespace private var dealingNamespace
#State private var dealt = Set<Int>()
private func deal(_ card: DataModel.Card) {
dealt.insert(card.id)
}
private func isUnDealt(_ card: DataModel.Card) -> Bool {
!dealt.contains(card.id)
}
let columns = [
GridItem(.adaptive(minimum: 80))
]
var body: some View {
VStack {
VStack {
Text("Example - \(example.cardsDisplayed.count) cards Displayed")
LazyVGrid(columns: columns, spacing: 10) {
ForEach(example.cardsDisplayed) { card in
CardView(card: card)
.aspectRatio(2/3, contentMode: .fit)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
.onTapGesture {
example.choose(card)
}
.padding(4)
}
}
Spacer()
HStack {
deckBody
Spacer()
}
}
}
}
var deckBody: some View {
ZStack {
ForEach(example.cardsInDeck) { card in
CardView(card: card)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
}
}
.frame(width: 60, height: 90)
.foregroundColor(.cyan)
.onTapGesture {
withAnimation {
example.dealMoreCards() // Make the next group of cards available (12 or 3)
// deal cards
for card in example.cardsDealt {
if isUnDealt(card) {
deal(card)
}
}
}
}
}
}
struct CardView: View {
let card: DataModel.Card
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill()
.foregroundColor(card.wasDealt ? .white : .cyan)
RoundedRectangle(cornerRadius: 5)
.strokeBorder(lineWidth: 3)
Text("Card: \(card.id)")
.padding(4)
}
}
}
A quick note about naming conventions. Types and structs should all be capitalized, so ExampleView and DataModel would be convention, and I changed the code to reflect that. You will need to fix yours when you incorporate this code.
Also note that this view isn't perfect. Eventually, it will push the deck of cards off the bottom of the screen, but that can be handled.
Your attempt to make the cards fly one at a time doesn't work. First, the withAnimation has to capture example.dealMoreCards() and trying to implement a .delay in that situation doesn't work. It can be implemented with a DispatchQueue.main.asyncAfter(deadline:), but you would have to rework how you pass the timing as the individual cards won't be available yet.
Lastly, the transitions do nothing in this situation.

SwiftUI - Random word animation with letters scrolling

I would like to implement a random word generator, shaped similar as to a Cryptex. I would like to have the single letters that rotate in a "wheel picker style" (as if the system was going through all the letters of the alphabet to pick the right ones) and that one at a time they'd stop to reveal the word.
I have implemented a string that gets randomized, with the single letters that all have individual 3d rotation animation. However, I have no idea on how to go from here to the result I'd like to have. In the code listed here below there's also a function that I created to generate the delay, so each letter could animate a bit longer. I tried adding this func to the duration value of the Animation, but with that the animation stops working. I left the func anyway, in case it might be useful.
import SwiftUI
import Foundation
struct ContentView: View {
#State private var degrees = 0.0
var words = ["One", "Two", "Three", "Four", "Five", "Six", "Seven"]
#State var text = "???"
var body: some View {
VStack {
HStack(spacing: 0) {
ForEach(Array(text), id: \.self) { letters in
Text(String(letters))
.fontWeight(.semibold)
.foregroundColor(.green)
.font(.system(size: 30))
.rotation3DEffect(.degrees(degrees), axis: (x: 1, y: 0, z: 0))
.animation(Animation.linear(duration: 3).speed(10), value: degrees)
}
}
Button {
withAnimation {
self.degrees += 720
let randomIndex = Int.random(in: 0...(words.count-1))
text = words[randomIndex]
}
} label: {
Text("Get a random word!")
.foregroundColor(.black)
}
.padding(.top, 40)
}
}
func generateDelay(letter: String.Element) -> Double {
let delayFromIndex : Double = Double(Array(text).firstIndex(where: { $0 == letter }) ?? 0)
let delay : Double = 3 + delayFromIndex
return delay
}
}
extension ForEach where Data.Element: Hashable, ID == Data.Element, Content: View {
init(values: Data, content: #escaping (Data.Element) -> Content) {
self.init(values, id: \.self, content: content)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Xcode RealityKit / Failed to produce diagnostic for expression

Im trying to make Augmented Reality app with RealityKit but ContentView.swift I have some problems
What is missing here ?` You can see errors on picture which I shared. I followed some tutorial so Im new on Xcode and Realitykit.
Failed to produce diagnostic for expression; please file a bug report
Cannot find 'PlacementButtonsView' in scope
import SwiftUI
import RealityKit
struct ContentView : View {
var models: [String] = {
let filemanager = FileManager.default
guard let path = Bundle.main.resourcePath, let files = try?
filemanager.contentsOfDirectory(atPath:path) else
{ return[]
}
var avaliableModels: [String] = []
for filename in files where filename.hasSuffix("usdz") {
let modelName = filename.replacingOccurrences(of: ".usdz", with: "")
avaliableModels.append(modelName)
}
return avaliableModels
}()
var body: some View {
ZStack(alignment: .bottom) {
ARViewContainer()
ModelPickerView(models: self.models)
PlacementButtonsView()
}
}
}
struct ARViewContainer: UIViewRepresentable {
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {}
}
struct ModelPickerView: View {
var models: [String]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
ForEach(0 ..<
self.models.count) { index in
Button(action: {
print("DEBUG: selected model with name: \(self.models[index])")
}) {
Image(uiImage: UIImage(named: self.models[index])!)
.resizable()
.frame(height: 60)
.aspectRatio(1/1,contentMode: .fit)
.background(Color.white)
.cornerRadius(12)
}
.buttonStyle (PlainButtonStyle())
}
}
}
.padding(15)
.background(Color.black.opacity(0.5))
}
struct PlacementButtonsView: View {
var body: some View {
HStack {
//Cancel Button
Button(action: {
print("DEBUG: model placement canceled.")
}) {
Image(systemName: "xmark")
.frame(width: 60, height: 60)
.font(.title)
.background(Color.white.opacity(0.75))
.cornerRadius(30)
.padding(20)
}
//Confirm Button
Button(action: {
print("DEBUG: model placement confirmed.")
}) {
Image(systemName: "checkmark")
.frame(width: 60, height: 60)
.font(.title)
.background(Color.white.opacity(0.65))
.cornerRadius(30)
.padding(20)
}
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
}
Check your braces. The PlacementButtonsView struct is actually nested within the ModelPickerView struct, and not globally available, hence why it is not available from your ContentView.
By the way, in Xcode, you can find this out by option clicking on the declaration of PlacementButtonsView:
ModelPickerView.PlacementButtonsView shows you what went wrong here; PlacementButtonsView is nested within ModelPickerView. This is why you seem to have a strange closing brace on the final line of your code sample - the same issue occurs with the preview, as it is also nested in ModelPickerView.
To make this issue more visible, and see similar issues like this in the future more easily, you can also have Xcode indent your code for you by selecting all (Cmd + A) and then pressing Control + I. You'll see the PlacementButtonsView struct indent, making it more clear that it is not globally available.

SwiftUI - how know number of lines in Text?

I have a dynamic text, it can be small or large
I what by default show only 3 lines and only if needed add "more" button.
When the user tap on this button ("More") - I will show all test.
I ask, how to know in SwiftUI if text it more of 3 lines or not?
You can use a GeometryReader to determine the width of the text field and then use that together with information about the font to calculate the size of the bounding rect that would be required to show the entire text. If that height exceeds the text view then we know that the text has been truncated.
struct LongText: View {
/* Indicates whether the user want to see all the text or not. */
#State private var expanded: Bool = false
/* Indicates whether the text has been truncated in its display. */
#State private var truncated: Bool = false
private var text: String
init(_ text: String) {
self.text = text
}
private func determineTruncation(_ geometry: GeometryProxy) {
// Calculate the bounding box we'd need to render the
// text given the width from the GeometryReader.
let total = self.text.boundingRect(
with: CGSize(
width: geometry.size.width,
height: .greatestFiniteMagnitude
),
options: .usesLineFragmentOrigin,
attributes: [.font: UIFont.systemFont(ofSize: 16)],
context: nil
)
if total.size.height > geometry.size.height {
self.truncated = true
}
}
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(self.text)
.font(.system(size: 16))
.lineLimit(self.expanded ? nil : 3)
// see https://swiftui-lab.com/geometryreader-to-the-rescue/,
// and https://swiftui-lab.com/communicating-with-the-view-tree-part-1/
.background(GeometryReader { geometry in
Color.clear.onAppear {
self.determineTruncation(geometry)
}
})
if self.truncated {
self.toggleButton
}
}
}
var toggleButton: some View {
Button(action: { self.expanded.toggle() }) {
Text(self.expanded ? "Show less" : "Show more")
.font(.caption)
}
}
}
This is then how it looks for both long and short texts:
Hope this helps.
Building on the excellent work from bhuemer, this version respects SwiftUI's local Font rather than requiring a hard-coded UIFont. Rather than reading the size of the "full" text using String layout, this renders Text three times: once for real, once with a line limit, and once without a line limit. It then uses two GRs to compare the last two.
struct LongText: View {
/* Indicates whether the user want to see all the text or not. */
#State private var expanded: Bool = false
/* Indicates whether the text has been truncated in its display. */
#State private var truncated: Bool = false
private var text: String
var lineLimit = 3
init(_ text: String) {
self.text = text
}
var body: some View {
VStack(alignment: .leading) {
// Render the real text (which might or might not be limited)
Text(text)
.lineLimit(expanded ? nil : lineLimit)
.background(
// Render the limited text and measure its size
Text(text).lineLimit(lineLimit)
.background(GeometryReader { displayedGeometry in
// Create a ZStack with unbounded height to allow the inner Text as much
// height as it likes, but no extra width.
ZStack {
// Render the text without restrictions and measure its size
Text(self.text)
.background(GeometryReader { fullGeometry in
// And compare the two
Color.clear.onAppear {
self.truncated = fullGeometry.size.height > displayedGeometry.size.height
}
})
}
.frame(height: .greatestFiniteMagnitude)
})
.hidden() // Hide the background
)
if truncated { toggleButton }
}
}
var toggleButton: some View {
Button(action: { self.expanded.toggle() }) {
Text(self.expanded ? "Show less" : "Show more")
.font(.caption)
}
}
}
The following shows the behavior with surrounding views. Note that this approach supports LongText(...).font(.largeTitle) just like a regular Text.
struct ContentView: View {
let longString = "This is very long text designed to create enough wrapping to force a More button to appear. Just a little more should push it over the edge and get us to one more line."
var body: some View {
VStack {
Text("BEFORE TEXT")
LongText(longString).font(.largeTitle)
LongText(longString).font(.caption)
Text("AFTER TEXT")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here is an example:
struct TextDemo: View {
#State var moreText = true
var body: some View {
Group {
Button(action: { self.moreText.toggle()} ) { Text("More") }
Text("hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello")
.frame(width: 300)
.lineLimit( moreText ? 3: nil)
}
}
}

Animations triggered by events in SwiftUI

SwiftUI animations are typically driven by state, which is great, but sometimes you really want to trigger a temporary (often reversible) animation in response to some event. For example, I want to temporarily increase the size of a button when a it is tapped (both the increase and decrease in size should happen as a single animation when the button is released), but I haven't been able to figure this out.
It can sort of be hacked together with transitions I think, but not very nicely. Also, if I make an animation that uses autoreverse, it will increase the size, decrease it and then jump back to the increased state.
That is something I have been into as well.
So far my solution depends on applying GeometryEffect modifier and misusing the fact that its method effectValue is called continuously during some animation. So the desired effect is actually a transformation of interpolated values from 0..1 that has the main effect in 0.5 and no effect at 0 or 1
It works great, it is applicable to all views not just buttons, no need to depend on touch events or button styles, but still sort of seems to me as a hack.
Example with random rotation and scale effect:
Code sample:
struct ButtonEffect: GeometryEffect {
var offset: Double // 0...1
var animatableData: Double {
get { offset }
set { offset = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
let effectValue = abs(sin(offset*Double.pi))
let scaleFactor = 1+0.2*effectValue
let affineTransform = CGAffineTransform(rotationAngle: CGFloat(effectValue)).translatedBy(x: -size.width/2, y: -size.height/2).scaledBy(x: CGFloat(scaleFactor), y: CGFloat(scaleFactor))
return ProjectionTransform(affineTransform)
}
}
struct ButtonActionView: View {
#State var animOffset: Double = 0
var body: some View {
Button(action:{
withAnimation(.spring()) {
self.animOffset += 1
}
})
{
Text("Press ME")
.padding()
}
.background(Color.yellow)
.modifier(ButtonEffect(offset: animOffset))
}
}
You can use a #State variable tied to a longPressAction():
Code updated for Beta 5:
struct ContentView: View {
var body: some View {
HStack {
Spacer()
MyButton(label: "Button 1")
Spacer()
MyButton(label: "Button 2")
Spacer()
MyButton(label: "Button 3")
Spacer()
}
}
}
struct MyButton: View {
let label: String
#State private var pressed = false
var body: some View {
return Text(label)
.font(.title)
.foregroundColor(.white)
.padding(10)
.background(RoundedRectangle(cornerRadius: 10).foregroundColor(.green))
.scaleEffect(self.pressed ? 1.2 : 1.0)
.onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: { pressing in
withAnimation(.easeInOut(duration: 0.2)) {
self.pressed = pressing
}
}, perform: { })
}
}
I believe this is what you're after. (this is how I solved this problem)
Based on dfd's link in i came up with this, which is not dependent on any #State variable. You simply just implement your own button style.
No need for Timers, #Binding, #State or other complex workarounds.
import SwiftUI
struct MyCustomPressButton: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding(10)
.cornerRadius(10)
.scaleEffect(configuration.isPressed ? 0.8 : 1.0)
}
}
struct Play: View {
var body: some View {
Button("Tap") {
}.buttonStyle(MyCustomPressButton())
.animation(.easeIn(duration: 0.2))
}
}
struct Play_Previews: PreviewProvider {
static var previews: some View {
Play()
}
}
There is no getting around the need to update via state in SwiftUI. You need to have some property that is only true for a short time that then toggles back.
The following animates from small to large and back.
struct ViewPlayground: View {
#State var enlargeIt = false
var body: some View {
Button("Event!") {
withAnimation {
self.enlargeIt = true
}
}
.background(Momentary(doIt: self.$enlargeIt))
.scaleEffect(self.enlargeIt ? 2.0 : 1.0)
}
}
struct Momentary: View {
#Binding var doIt: Bool
var delay: TimeInterval = 0.35
var body: some View {
Group {
if self.doIt {
ZStack { Spacer() }
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + self.delay) {
withAnimation {
self.doIt = false
}
}
}
}
}
}
}
Unfortunately delay was necessary to get the animation to occur when setting self.enlargeIt = true. Without that it only animates back down. Not sure if that's a bug in Beta 4 or not.

Resources