I am currently trying to get a vertical flip animation for my card in my project's subview. I am using some dependencies (Shuffle Cocoapod), and I tried everything to attempt to get a flip animation from right to left of my card. It seems like I can change the background and other simple stuff of each single card, but nothing happens when trying to animate it. The goal would be to "show the back of the card" with another word in it (basically each card has a "front word" and a "back word". What am I getting wrong?
import UIKit
import Shuffle
class CardsViewController: UIViewController, SwipeCardStackDataSource, SwipeCardStackDelegate {
let cardStack = SwipeCardStack()
var actualIndex : Int = 0
var showingBack = false
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(cardStack)
cardStack.frame.size.height = 512
cardStack.frame.size.width = 384
cardStack.center.y = CGFloat(cardStack.frame.size.height/2)
cardStack.center.x = CGFloat(cardStack.frame.size.width/2)
cardStack.center = view.center
cardStack.dataSource = self
}
func card(fromImage word: String) -> SwipeCard {
let card = SwipeCard()
card.swipeDirections = [.left, .right]
for direction in card.swipeDirections {
card.setOverlay(CardOverlay(direction: direction), forDirection: direction)
}
card.content = CardContentView(withWord: word)
return card
}
func cardStack(_ cardStack: SwipeCardStack, cardForIndexAt index: Int) -> SwipeCard {
let theCard = card(fromImage: cardWords[actualIndex].word)
actualIndex = actualIndex + 1
return theCard
}
func numberOfCards(in cardStack: SwipeCardStack) -> Int {
return cardWords.count
}
let cardWords = [
DataOfWord(word: "comme", translation: "as"),
DataOfWord(word: "je", translation: "I"),
DataOfWord(word: "son", translation: "his"),
DataOfWord(word: "que", translation: "that")]
}
You can create a flip type of animation with the UIView's transition(with:duration:options:animations:completion:) method
Here's some code from one of my own apps where I flip the gameboard to display the level completion score on the "other side":
UIView.transition(with: gameBoardCollectionView, duration: 0.75, options: [.transitionFlipFromRight]) {
self.loadLevelEndArcadeView()
} completion: { (complete) in
self.animateLevelEndArcadeLabelViews()
}
So, inside the transition code block, you just load the views you want and when the animation completes, you'll be displaying something that represents the back side of a view. There are also other transition animations you can call upon if you want.
Related
I'm using a UIScrollView to display an image with various markers on top. The image view has a UILongPressGestureRecognizer that detects long presses. When the long press event is detected, I want to create a new marker at that location.
The problem I'm having is that when I zoom in or out, the location of the gesture recognizer's location(in: view) seems to be off. Here's a snippet of my implementation:
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.onLongPress(gesture:)))
self.hostingController.view.addGestureRecognizer(longPressGestureRecognizer)
#objc func onLongPress(gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
guard let view = gesture.view else { break }
let location = gesture.location(in: view)
let pinPointWidth = 32.0
let pinPointHeight = 42.0
let x = location.x - (pinPointWidth / 2)
let y = location.y - pinPointHeight
let finalLocation = CGPoint(x: x, y: y)
self.onLongPress(finalLocation)
default:
break
}
}
Please note that I'm using a UIViewControllerRepresentable that contains a UIViewController with a UIScrollView that is surfaced to my SwiftUI View. Maybe this might be causing it.
Here's the SwiftUI code:
var body: some View {
UIScrollViewWrapper(scaleFactor: $scaleFactor, onLongPress: onInspectionCreated) {
ZStack(alignment: .topLeading) {
Image(uiImage: image)
ForEach(filteredInspections, id: \.syncToken) { inspection in
InspectionMarkerView(
scaleFactor: scaleFactor,
xLocation: CGFloat(inspection.xLocation),
yLocation: CGFloat(inspection.yLocation),
iconName: iconNameForInspection(inspectionMO: inspection),
label: inspection.readableIdPaddedOrNewInspection)
.onTapGesture {
selectedInspection = inspection
}
}
}
}
.clipped()
}
Here's a link to a reproducible example project:
https://github.com/Kukiwon/sample-project-zoom-long-press-location
Here's a recording of the problem:
Link to video
Any help is greatly appreciated!
I don't use SwiftUI, but I've seen some quirky stuff looking at UIHostingController implementations, and it appears a specific quirk is hitting you.
I inset your scroll view by 40-pts and gave the main view a red background to make it a little easier to see what's going on.
First, add a 2-pixel blue line around the border of your sheet_hd image, and scroll all the way to the bottom-left corner. It should look like this:
As you zoom in, keeping the scroll at bottom-left, it will look like this:
So far, so good -- and using a long-press to add a marker works as expected.
However, as soon as we zoom out to less than 1.0 zoom scale:
we can no longer see the bottom edge of the image.
Zooming back in makes it more obvious:
And the long-press location is incorrect.
For further clarification, if we set .clipsToBounds = false on the scroll view, and set .alpha = 0.5 on the image view, we see this:
We can drag the view up to see the bottom edge, but as soon as we release the touch is bounces back below the frame of the scroll view.
What should fix this is to use this extension:
// extension to remove safe area from UIHostingController
// source: https://stackoverflow.com/a/70339424/6257435
extension UIHostingController {
convenience public init(rootView: Content, ignoreSafeArea: Bool) {
self.init(rootView: rootView)
if ignoreSafeArea {
disableSafeArea()
}
}
func disableSafeArea() {
guard let viewClass = object_getClass(view) else { return }
let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
if let viewSubclass = NSClassFromString(viewSubclassName) {
object_setClass(view, viewSubclass)
}
else {
guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
let safeAreaInsets: #convention(block) (AnyObject) -> UIEdgeInsets = { _ in
return .zero
}
class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
}
objc_registerClassPair(viewSubclass)
object_setClass(view, viewSubclass)
}
}
}
Then, in viewDidLoad() in your UIScrollViewViewController:
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.scrollView)
self.pinEdges(of: self.scrollView, to: self.view)
// add this line
self.hostingController.disableSafeArea()
self.hostingController.willMove(toParent: self)
self.scrollView.addSubview(self.hostingController.view)
self.pinEdges(of: self.hostingController.view, to: self.scrollView)
self.hostingController.didMove(toParent: self)
self.hostingController.view.alpha = 0
}
Quick testing (obviously, you'll want to thoroughly test it) seems good... we can scroll all the way to the bottom, and long-press location is back where it should be.
Background:
I'm trying to animate a wave using BAFluidView. My goal is pretty simple: start the animation when a button is tapped, stop it when it is tapped again. The pod (linked above) provides all of the code to manage this.
Let it be known, I'm new to this. Help me learn, please!
My struggle:
I need to create a view for this wave. I put it into a function called "WaveContainer." Here's what that looks like:
func WaveAnimation() {
let wave = BAFluidView(frame: self.view.frame, startElevation: 0.02)!
wave.maxAmplitude = 10
wave.minAmplitude = 8
wave.fillDuration = 50
wave.fill(to: 0.95)
wave.fillAutoReverse = false
wave.fillColor = UIColor.blue
waveView.addSubview(wave)
}
I then called this in the ViewDidAppear function. Of course, it works! I can see the wave, and it's waving. Nice.
Of course, I can't call the wave constant anywhere else, though! If I want to stop / start the wave on a button press, for example?
If I try to move the wave constant out of this function into ViewDidLoad or ViewDidAppear, I can't access the ...self.view.frame, and the wave won't show up on the screen.
My asks:
How should I structure this code so that I can reference the wave constant from several different functions?
How should I reference the view for the wave constant when I'm not within ViewDidLoad or some other view-based function?
Thank you SO MUCH for your help!
My Code:
import UIKit
import BAFluidView
class ViewController: UIViewController {
// OUTLETS:
#IBOutlet weak var waveView: UIView!
// ACTIONS:
#IBAction func WaveButton(_ sender: UIButton) {
// If the user taps this button and the waveHasStarted variable is equal to false, flip it to true.
if waveHasStarted == false {
print("Start the wave with a press.")
startWave = true
waveHasStarted = true
waveHasBeenStopped = false
} else {
print("Stop the wave with a press.")
startWave = false
waveHasStarted = false
waveHasBeenStopped = true
}
}
// VARIABLES:
var waveHasStarted = false
var startWave = false
var waveHasBeenStopped = true
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {}
// FUNCTIONS:
func WaveAnimation() {
let wave = BAFluidView(frame: self.view.frame, startElevation: 0.02)!
wave.maxAmplitude = 10
wave.minAmplitude = 8
wave.fillDuration = 50
wave.fill(to: 0.95)
wave.fillAutoReverse = false
wave.fillColor = UIColor.blue
// If the variable above has been flipped to "true," start the animation...
if startWave == true {
print("Start that wave animation")
wave.startAnimation()
} else {
// If not, stop it or do nothing.
print("Stop that wave animation")
wave.stopAnimation()
wave.keepStationary()
}
waveView.addSubview(wave)
}
}
Use lazy initialization and get the class level access,
lazy var fluidView: BAFluidView = {
let wave = BAFluidView(frame: self.view.frame, startElevation: 0.02)!
wave.maxAmplitude = 10
wave.minAmplitude = 8
wave.fillDuration = 50
wave.fill(to: 0.95)
wave.fillAutoReverse = false
wave.fillColor = UIColor.blue
return wave
}()
Now you can add, start and stop the animation anywhere as,
waveView.addSubview(fluidView)
fluidView.startAnimation()
fluidView.stopAnimation()
I am trying to swipe up to a particular element in my app, if i use "swipeup" it is going to bottom of the view, which i don't want
Here is my code :
XCUIElement *staticText = [[[tablesQuery2 childrenMatchingType:XCUIElementTypeCell] elementBoundByIndex:2] childrenMatchingType:XCUIElementTypeStaticText].element;
[staticText swipeUp];
Here is my app screen before using swipe up
Here is my app screen after using swipe up
If you know which element you want to select and the height of each picker item, you can make an extension of XCUIElement to select the right value.
/// Move up/down the picker options until the given `selectionPosition` is reached.
func changePickerSelection(pickerWheel: XCUIElement, selectionPosition: UInt) {
// Select the new value
var valueSelected = false
while !valueSelected {
// Get the picker wheel's current position
if let pickerValue = pickerWheel.value {
let currentPosition = UInt(getPickerState(String(pickerValue)).currentPosition)
switch currentPosition.compared(to: selectionPosition) {
case .GreaterThan:
pickerWheel.selectPreviousOption()
case .LessThan:
pickerWheel.selectNextOption()
case .Equal:
valueSelected = true
}
}
}
}
/// Extend XCUIElement to contain methods for moving to the next/previous value of a picker.
extension XCUIElement {
/// Scrolls a picker wheel up by one option.
func selectNextOption() {
let startCoord = self.coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.5))
let endCoord = startCoord.coordinateWithOffset(CGVector(dx: 0.0, dy: 30.0)) // 30pts = height of picker item
endCoord.tap()
}
/// Scrolls a picker wheel down by one option.
func selectPreviousOption() {
let startCoord = self.coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.5))
let endCoord = startCoord.coordinateWithOffset(CGVector(dx: 0.0, dy: -30.0))
endCoord.tap()
}
}
let pickerWheel = app.pickerWheels.element(boundBy: 0)
changePickerSelection(pickerWheel, selectionPosition: 2)
Im trying to make a small game. And there is some problem with the animation. Im new to Swift. So lets take a look. I create a UIImageView picture and want to do animation of this picture appear in a different places on a screen. I believe that the algorithm will look like this:
Infinite loop{
1-GetRandomPlace
2-change opacity from 0 to 1 and back(with smooth transition)
}
Looks simple, but I can't understand how to do it correctly in Xcode.
Here is my test code but it looks useless
Thank you for help and sorry if there was already this question, I can't find it.
class ViewController: UIViewController {
#IBOutlet weak var BackgroundMainMenu:UIImageView!
#IBOutlet weak var AnimationinMenu: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// MoveBackgroundObject(AnimationinMenu)
// AnimationBackgroundDots(AnimationinMenu, delay: 0.0)
// self.AnimationinMenu.alpha = 0
//
// UIImageView.animateWithDuration(3.0,
// delay: 0.0,
// options: UIViewAnimationOptions([.Repeat, .CurveEaseInOut]),
// animations: {
// self.MoveBackgroundObject(self.AnimationinMenu)
// self.AnimationinMenu.alpha = 1
// self.AnimationinMenu.alpha = 0
// },
// completion: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(animated: Bool) {
AnimationBackgroundDots(AnimationinMenu, delay: 0.0)
}
func ChangeOpacityto1(element: UIImageView){
element.alpha = 0
UIView.animateWithDuration(3.0) {
element.alpha = 1
}
}
func ChangeOpacityto0(element: UIImageView){
element.alpha = 1
UIView.animateWithDuration(3.0){
element.alpha = 0
}
}
func AnimationBackgroundDots(element: UIImageView, delay: Double){
element.alpha = 0
var z = 0
while (z<4){
MoveBackgroundObject(AnimationinMenu)
UIImageView.animateWithDuration(3.0,
animations: {
element.alpha = 0
element.alpha = 1
element.alpha = 0
},
completion: nil)
z++
}
}
func MoveBackgroundObject(element: UIImageView) {
// Find the button's width and height
let elementWidth = element.frame.width
let elementHeight = element.frame.height
// Find the width and height of the enclosing view
let viewWidth = BackgroundMainMenu.superview!.bounds.width
let viewHeight = BackgroundMainMenu.superview!.bounds.height
// Compute width and height of the area to contain the button's center
let xwidth = viewWidth - elementWidth
let yheight = viewHeight - elementHeight
// Generate a random x and y offset
let xoffset = CGFloat(arc4random_uniform(UInt32(xwidth)))
let yoffset = CGFloat(arc4random_uniform(UInt32(yheight)))
// Offset the button's center by the random offsets.
element.center.x = xoffset + elementWidth / 2
element.center.y = yoffset + elementHeight / 2
}
}
The problem is in AnimationBackgroundDots.
You are immediately creating 4 animations on the same view but only one can run at a time. What you need to do is wait until one animation is finished (fade in or fade out) before starting a new one.
Also, the animations closure is for setting the state you want your view to animate to. It looks at how your view is at the start, runs animations, then looks at the view again and figures out how to animate between the two. In your case, the alpha of the UIImageView starts at 0, then when animations runs, the alpha ends up being 0 so nothing would animate. You can't create all the steps an animation should take that way.
Want you need to do it move your view and start the fade in animation. The completion closure of fading in should start the fade out animation. The completion closure of the fading out should then start the process all over again. It could look something like this.
func AnimationBackgroundDots(element: UIImageView, times: Int) {
guard times > 0 else {
return
}
MoveBackgroundObject(element)
element.alpha = 1
// Fade in
UIView.animateWithDuration(3.0, animations: {
element.alpha = 1
}, completion: { finished in
// Fade Out
UIView.animateWithDuration(3.0, animations: {
element.alpha = 0
}, completion: { finished in
// Start over again
self.AnimationBackgroundDots(element, times: times-1)
})
})
}
You called also look at using keyframe animations but this case is simple enough that theres no benefit using them.
Also as a side note. The function naming convention in swift is to start with a lowercase letter, so AnimationBackgroundDots should be animationBackgroundDots.
I'm making an a game in XCode 7 using Swift 2. I have a variable that I want to pass from the start screen (which is an UIViewController) to the game scene (which is an SKScene). I want the player to select a character in a UIView and play with it in the SKScene. I also want the score that's in the SKScene to show in the game-over screen that's an UIView. I've seen tutorials for passing data between two UIViewControllers and between two SKScenes, but none of them work for this case.
How can I pass a variable from an UIViewController to a SKScene (and vice versa)?
I ran over this same problem yesterday.
Swift 5.2, xcode 12 and targeting iOS 14. Searched high and low. Eventually found the userData attribute of the SKScene.
Note: The app is based on SwiftUI and the SKScene is inside a UIViewRepresentable:
struct SpriteKitContainer: UIViewRepresentable {
typealias UIViewType = SKView
var skScene: SKScene!
#Binding var timerData : ModelProgressTimer
Not that I am passing a binding into the View - this contains a number of data items I wanted to make available to the scene.
I placed code in two places to populate skScene.userData:
func makeUIView(context: Context) -> SKView {
self.skScene.scaleMode = .resizeFill
self.skScene.backgroundColor = .green
debugPrint("setting user data")
self.skScene.userData = [:]
self.skScene.userData!["color"] = UIColor(timerData.flatColorTwo!)
self.skScene.userData!["running"] = timerData.isRunning
self.skScene.userData!["percent"] = timerData.percentComplete
let view = SKView(frame: .zero)
view.preferredFramesPerSecond = 60
// view.showsFPS = true
// view.showsNodeCount = true
view.backgroundColor = .clear
view.allowsTransparency = true
return view
}
func updateUIView(_ view: SKView, context: Context) {
self.skScene.userData = [:]
self.skScene.userData!["running"] = timerData.isRunning
self.skScene.userData!["color"] = UIColor(timerData.flatColorTwo!)
self.skScene.userData!["percent"] = timerData.percentComplete
view.presentScene(context.coordinator.scene)
}
Then, inside the skScene object, I am able to retrieve the userData values:
var item: SKShapeNode! = nil
override func sceneDidLoad() {
let scaleUp = SKAction.scale(by: 2.0, duration: 1.0)
let scaleDown = SKAction.scale(by: 0.5, duration: 1.0)
scaleUp.timingMode = .easeInEaseOut
scaleDown.timingMode = .easeInEaseOut
let sequence = SKAction.sequence([scaleUp,scaleDown])
actionRepeat = SKAction.repeatForever(sequence)
item = SKShapeNode(circleOfRadius: radius)
addChild(item)
}
override func didChangeSize(_ oldSize: CGSize) {
item.fillColor = self.userData!["color"] as! UIColor
item.strokeColor = .clear
let meRunning = self.userData!["running"] as! Bool
if meRunning {
item.run(actionRepeat)
} else {
item.removeAllActions()
}
}
This draws a circle that "pulses" if the "running" boolean is set in the userdata (a Bool value).
If you haven't already found the answer, I did find a way to do this. I was doing something very similar.
In my case I have a UIView that gets called as a pause menu from my scene. I have made the class and variable names a little more ambiguous so they can apply to your situation as well.
class MyView: UIView {
var scene: MyScene!
var button: UIButton!
init(frame: CGRect, scene: MyScene){
super.init(frame: frame)
self.scene = scene
//initialize the button
button.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "buttonTapped"))
}
func buttonTapped(){
self.removeFromSuperview() // this removes my menu from the scene
scene.doTheThing() // this calls the method on my scene
}
Here are the relevant parts of my scene.
class MyScene: SKScene {
func doTheThing(){
//this is a function on the scene. You can pass any variable you want through the function.
}
}
In your situation, it sounds like the first screen is a UIView and the second screen is the SKScene.
You may want to make your SKScene first, pause it, and then add the UIView in front of the Scene. Once the character is selected, you can remove the UIView and add your character into the scene.
I hope that this answers your question. If it doesn't let me know.
From the level select scene:
let scene = GameScene(fileNamed:"GameScene")
scene?.userData = [:]
scene?.userData!["level"] = 21
self.view?.presentScene(scene!)
From within the game scene:
let level = self.userData!["level"] as! Int