slide transition using UIView.transition() method - ios

I have and table view and when the user clicks a button I want it to reload it's data and change numerous other things. Sadly, my users have not been satisfied with the available options for the transitions/animations, and wanted a sliding transition. I couldn't find anything online to do this, however. Here is my code:
UIView.transition(with: view, duration: 0.5,options: .transitionCurlUp,animations:
{ () -> Void in
self.schedule.reloadData()
self.formatter.dateFormat = "yyyy-MM-dd"
let dayOfWeek = self.week[self.getDayOfWeek(self.currentDate)!-1]
var forReturn = dayOfWeek.getCourseNumber()
if forReturn>3{
forReturn = forReturn + 1
self.refreshButton.isHidden = false
self.noClasses.isHidden = true
}else{
self.refreshButton.isHidden = true
self.noClasses.isHidden = false
}
self.changeWeek()
let startDate = self.formatter.string(from: self.startDateBase)
if self.currentDate == startDate{
self.other1.text = "Today's Schedule"
}else{
self.formatter.dateFormat = "yyyy-MM-dd"
self.currentDate = self.formatter.string(from: self.date)
self.other1.text = dayNames[self.getDayOfWeek(self.currentDate)!-1] + " Schedule"
}
}, completion: nil
);
Thank you.

You can go old school on this:
take a snapshot of the view;
add it to the view hierarchy;
apply offsetting transforms to the snapshot view and the view, itself;
animate the restoration of the main view's transform back to .identity; and
removing the snapshot view when done with the animation.
Thus:
let snapshotView = snapshot()
view.addSubview(snapshotView)
snapshotView.transform = CGAffineTransform(translationX: -view.bounds.size.width, y: 0)
view.transform = CGAffineTransform(translationX: view.frame.size.width, y: 0)
// do whatever updates to the views you want here
tableView.reloadData()
UIView.animate(withDuration: 0.5, animations: {
self.view.transform = .identity
}, completion: { _ in
snapshotView.removeFromSuperview()
})
For the snapshot, I used:
func snapshot() -> UIView {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, true, 0)
view.drawHierarchy(in: view.bounds, afterScreenUpdates: false)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let imageView = UIImageView(image: image)
imageView.frame = view.bounds
return imageView
}
By the way, you can also use snapshotView(afterScreenupdates:), which is faster than drawHierarchy, but it doesn't work in 7+ simulator for some reason (see https://forums.developer.apple.com/thread/63438), so I'm not 100% comfortable with it. But if you wanted to do that, you'd do replace the shapshotView declaration with:
let snapshotView = view.snapshotView(afterScreenUpdates: false)!

Related

How to use completion block for UIView.animate()?

I'm working on a project to learn animations and am having trouble using the completion block for the UIView.animate(withDuration:) func. MY animation is a shoebox that falls from the top of the screen, lands on a pedestal, then opens. Once the box opens I want a UIImageView to come out of the box, grow to full size of the screen and then segue to the next page, but the completion handler that the code for segueing is called before my animation completes and the UIImageView doesn't appear at all.
Here's my code:
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 1.5, delay: 0.0, options: .curveEaseInOut,
animations: {
//Opening the box
self.shoeBoxImage.shoeBox.animationImages = self.boxOpeningAnimation
self.shoeBoxImage.shoeBox.animationDuration = 1.5
self.shoeBoxImage.shoeBox.animationRepeatCount = 1
self.shoeBoxImage.shoeBox.contentMode = .scaleAspectFill
self.shoeBoxImage.shoeBox.startAnimating()
//set to the final image
self.shoeBoxImage.shoeBox.image = UIImage(named: "frame13")
},completion: {_ in
let nextPage = UIImageView()
nextPage.frame = CGRect(origin: self.shoeBoxImage.center, size: CGSize(width: 0.0, height: 0.0))
nextPage.image = UIImage(named: "FirstLoadBackgroundImg.jpeg")
nextPage.autoresizesSubviews = true
self.view.addSubview(nextPage)
self.view.bringSubviewToFront(nextPage)
UIView.animate(withDuration: 5.0, animations: {
nextPage.transform = CGAffineTransform(scaleX: 428, y: 926)
})
self.performSegue(withIdentifier: "FinishedLoading", sender: self)
})
}
This is my first time working with animations and programatically creating views so if someone could explain how to make the completion block wait for the animation to complete. In order to make the UIImageView appear and animate then once it's full screen, segue to the next page it would be very much appreciated.
The size is 0, 0. Transforming zero by any scale is still zero. I would advise you to not use transform at all, but rather just set the final frame to be what you want.
E.g.,
let startFrame = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
let endFrame = view.bounds
let imageView = UIImageView(image: ...)
imageView.contentMode = .scaleAspectFill
view.addSubview(imageView)
imageView.frame = startFrame
UIView.animate(withDuration: 3, delay: 0, options: .curveEaseInOut) {
imageView.frame = endFrame
} completion: { _ in
// do something here
}
That yields:
By the way, the performSegue probably should be inside a completion closure of the inner animate call.

UIAnimation works normally but stops working after i exit from the view(it enters background) where it is placed and enter again(enters foreground)

i am presently running an animation that makes an image move on screen, it works but i noticed that anytime i run the app for the first time, it works normally but when i exit the view and re-enter the view of the animation, it never runs again. i have tried placing the function that runs the animation in different view controller life cycles but no improvement. code is below.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
moveIt(rootView.eclipseImageView)
applyGradient()
}
func moveIt(_ imageView: UIImageView) {
let date = Date()
let calendar = Calendar.current
hour = calendar.component(.hour, from: date)
let viewWidth = view.frame.size.width
let fullDayCycle = 24
let secondsInAnHour = 3600
let hoursLeftToMoveSun = fullDayCycle - hour
let totalSecondsLeftToMoveSun = secondsInAnHour * hoursLeftToMoveSun
let moveSunBy = (Int(viewWidth) * hoursLeftToMoveSun) / 24
let moveSunByInCGFloat = CGFloat(moveSunBy)
UIView.animate(withDuration: 30, delay: 0, options: [.curveLinear, .repeat, .autoreverse], animations: {
imageView.frame.origin.x = viewWidth // moveSunByInCGFloat
// self.view.backgroundColor = UIColor(hexaString: "#1f0001")
}, completion: { [weak self] (_) in
self?.moveIt(imageView)
})
}
func applyGradient() {
let gradient = CAGradientLayer()
gradient.frame = view.bounds
let hour = 4
gradient.colors = switchNewColors(hour)
gradient.transform = CATransform3DMakeRotation(CGFloat.pi, 0, 0, 1)
gradient.shouldRasterize = true
view.layer.insertSublayer(gradient, at: 0) // addSublayer(gradient)
}
Your code animates your imageView from it's original frame to a frame with origin.x at viewWidth. Once you've run that animation, your imageView's frame's origin.x will be equal to viewWidth forever after. Future calls to moveIt() won't change anything because you've already changed the imageView's frame.
Further, your animation's completion closure calls moveIt(_:) again, which also won't change anything.
What do you want new calls to moveIt(_:) to do?

Is it possible to fade between two view background images?

I would like to know if it's possible for some button to cause a fade from one background image to another? The following code I have results in an abrupt change from the current image, background-image-1, to the new image, background-image-2.
func buttonPressed() {
if let image = UIImage(named: "background-image-2") {
UIView.animateWithDuration(2, delay: 0, options: .CurveEaseInOut, animations: {_ in
self.view.backgroundColor = UIColor(patternImage: image)
}, completion: nil)
}
Thanks
EDIT
Though the accepted answer does work, I was finding slight glitches when toggling quickly between images. A similar method using UIView.animateWithDuration solves this:
func buttonPressed() {
let oldImageView = UIImageView(image: UIImage(named: "background-image-1"))
oldImageView.frame = view.frame
let newImageView = UIImageView(image: UIImage(named:"background-image-2"))
newImageView.frame = view.frame
newImageView.alpha = 0
view.insertSubview(newImageView, aboveSubview: backgroundImageView)
view.insertSubview(oldImageView, aboveSubview: newImageView)
UIView.animateWithDuration(1.0, delay: 0.0, options: .CurveEaseInOut, animations: {
oldImageView.alpha = 0
newImageView.alpha = 1
}, completion: { completed in
self.backgroundImageView.image = newImage
newImageView.removeFromSuperview()
oldImageView.removeFromSuperview()
})
}
Say imageView is the UIImageView you are using for your animation. image1 is your original image. image2 is the new image. Try this:
let crossFade: CABasicAnimation = CABasicAnimation(keyPath: "contents")
crossFade.duration = 1
crossFade.fromValue = image1.CGImage
crossFade.toValue = image2.CGImage
self.imageView.image = image2
self.imageView.layer.addAnimation(crossFade, forKey: "animateContents")
Hope this helps.
The background color is supposed to be an animatable property, but a pattern color must not work.
Do you actually want a pattern color, or are you using that in order to insert an image as the background of the view without using the repeating pattern?

How to capture a full page screenshot of WKWebview?

I can capture all content screenshot of UIWebView by adjust frame of UIScrollView of UIWebView. However, I can't capture all content screenshot of WKWebView by the same method.
The method I used for capture UIWebView as follow:
backup frame and superview of webview.scrollView
create a new container view as webview.scrollView's superview
adjust frame of new container view and webview.scrollView. frame is same to webview.scrollView.contentSize
draw by renderInContext or drawViewHierarchyInRect
However, this method will capture a white screenshot of WKWebview. It doesn't work!
I had print all level of WKWebView, then I found a UIView(WKContentView)'s size is same to contentView, you can found this view by this level:
WKWebView
WKScrollView
WKContentView(size is same to contentView)
I also had try to capture by WKContentView, then I found only visible view could be captured.
Anyway, Anyone could tell me how to capture a full page content screenshot of WKWebView?
Please refer to this answer
iOS 11.0 and above, Apple has provided following API to capture snapshot of WKWebView.
#available(iOS 11.0, *)
open func takeSnapshot(with snapshotConfiguration: WKSnapshotConfiguration?, completionHandler: #escaping (UIImage?, Error?) -> Swift.Void)
Swift 3, Xcode 8
func takeScreenshot() -> UIImage? {
let currentSize = webView.frame.size
let currentOffset = webView.scrollView.contentOffset
webView.frame.size = webView.scrollView.contentSize
webView.scrollView.setContentOffset(CGPoint.zero, animated: false)
let rect = CGRect(x: 0, y: 0, width: webView.bounds.size.width, height: webView.bounds.size.height)
UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.main.scale)
webView.drawHierarchy(in: rect, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
webView.frame.size = currentSize
webView.scrollView.setContentOffset(currentOffset, animated: false)
return image
}
Swift 3, Xcode 9:
I created an extension to achieve this. Hope this helps you.
extension WKWebView{
private func stageWebViewForScreenshot() {
let _scrollView = self.scrollView
let pageSize = _scrollView.contentSize;
let currentOffset = _scrollView.contentOffset
let horizontalLimit = CGFloat(ceil(pageSize.width/_scrollView.frame.size.width))
let verticalLimit = CGFloat(ceil(pageSize.height/_scrollView.frame.size.height))
for i in stride(from: 0, to: verticalLimit, by: 1.0) {
for j in stride(from: 0, to: horizontalLimit, by: 1.0) {
_scrollView.scrollRectToVisible(CGRect(x: _scrollView.frame.size.width * j, y: _scrollView.frame.size.height * i, width: _scrollView.frame.size.width, height: _scrollView.frame.size.height), animated: true)
RunLoop.main.run(until: Date.init(timeIntervalSinceNow: 1.0))
}
}
_scrollView.setContentOffset(currentOffset, animated: false)
}
func fullLengthScreenshot(_ completionBlock: ((UIImage) -> Void)?) {
// First stage the web view so that all resources are downloaded.
stageWebViewForScreenshot()
let _scrollView = self.scrollView
// Save the current bounds
let tmp = self.bounds
let tmpFrame = self.frame
let currentOffset = _scrollView.contentOffset
// Leave main thread alone for some time to let WKWebview render its contents / run its JS to load stuffs.
mainDispatchAfter(2.0) {
// Re evaluate the size of the webview
let pageSize = _scrollView.contentSize
UIGraphicsBeginImageContext(pageSize)
self.bounds = CGRect(x: self.bounds.origin.x, y: self.bounds.origin.y, width: pageSize.width, height: pageSize.height)
self.frame = CGRect(x: self.frame.origin.x, y: self.frame.origin.y, width: pageSize.width, height: pageSize.height)
// Wait few seconds until the resources are loaded
RunLoop.main.run(until: Date.init(timeIntervalSinceNow: 0.5))
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
// reset Frame of view to origin
self.bounds = tmp
self.frame = tmpFrame
_scrollView.setContentOffset(currentOffset, animated: false)
completionBlock?(image)
}
}
}
The key here is to let capture after webkit be allowed time to render after it is given a new frame. I tried didFinishLoading calback and WKWebView's wkWebView.takeSnapshot, but did not work.
So I introduced an a delay for the screenshot:
func createImage(webView: WKWebView, completion: #escaping (UIImage?) -> ()) {
// save the original size to restore
let originalFrame = webView.frame
let originalConstraints = webView.constraints
let originalScrollViewOffset = webView.scrollView.contentOffset
let newSize = webView.scrollView.contentSize
// remove any constraints for the web view, and set the size
// to be size of the content size (will be restored later)
webView.removeConstraints(originalConstraints)
webView.translatesAutoresizingMaskIntoConstraints = true
webView.frame = CGRect(origin: .zero, size: newSize)
webView.scrollView.contentOffset = .zero
// wait for a while for the webview to render in the newly set frame
DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) {
defer {
UIGraphicsEndImageContext()
}
UIGraphicsBeginImageContextWithOptions(newSize, false, 0)
if let context = UIGraphicsGetCurrentContext() {
// render the scroll view's layer
webView.scrollView.layer.render(in: context)
// restore the original state
webView.frame = originalFrame
webView.translatesAutoresizingMaskIntoConstraints = false
webView.addConstraints(originalConstraints)
webView.scrollView.contentOffset = originalScrollViewOffset
if let image = UIGraphicsGetImageFromCurrentImageContext() {
completion(image)
} else {
completion(nil)
}
}
}
}
Result:
- xcode 10 & swift 4.2 -
Use this;
// this is the button which takes the screenshot
#IBAction func snapShot(_ sender: Any) {
captureScreenshot()
}
// this is the function which is the handle screenshot functionality.
func captureScreenshot(){
let layer = UIApplication.shared.keyWindow!.layer
let scale = UIScreen.main.scale
// Creates UIImage of same size as view
UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, scale);
layer.render(in: UIGraphicsGetCurrentContext()!)
let screenshot = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// THIS IS TO SAVE SCREENSHOT TO PHOTOS
UIImageWriteToSavedPhotosAlbum(screenshot!, nil, nil, nil)
}
and you must add these keys in your info.plist;
key value = "Privacy - Photo Library Additions Usage Description" ,
key value = Privacy - Photo Library Usage Description
Here is an iOS 11+ example for snapshotting the WKWebView without the need of any delays to render the content.
The key is to make sure that you dont receive a blank snapshot. Initially we had issues with the output image being blank and having to introduce a delay to have the web content in the output image. I see the solution with the delay in many posts and I would recommend to not use it because it is an unnecessarily unreliable and unperformant solution. The important solution for us was to set the rect of WKSnapshotConfiguration and to use the JavaScript functions to wait for the rendering and also receiving the correct width and height.
Setting up the WKWebView:
// Important: You can set a width here which will also be reflected in the width of the snapshot later
let webView = WKWebView(frame: .zero)
webView.configuration.dataDetectorTypes = []
webView.navigationDelegate = self
webView.scrollView.contentInsetAdjustmentBehavior = .never
webView.loadHTMLString(htmlString, baseURL: Bundle.main.resourceURL)
Capturing the snapshot:
func webView(
_ webView: WKWebView,
didFinish navigation: WKNavigation!
) {
// Wait for the page to be rendered completely and get the final size from javascript
webView.evaluateJavaScript("document.readyState", completionHandler: { [weak self] (readyState, readyStateError) in
webView.evaluateJavaScript("document.documentElement.scrollWidth", completionHandler: {(contentWidth, widthError) in
webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (contentHeight, heightError) in
guard readyState != nil,
let contentHeight = contentHeight as? CGFloat,
let contentWidth = contentWidth as? CGFloat else {
[Potential error handling...]
return
}
let rect = CGRect(
x: 0,
y: 0,
width: contentWidth,
height: contentHeight
)
let configuration = WKSnapshotConfiguration()
configuration.rect = rect
if #available(iOS 13.0, *) {
configuration.afterScreenUpdates = true
}
webView.takeSnapshot(with: configuration) { (snapshotImage, error) in
guard let snapshotImage = snapshotImage else {
[Potential error handling...]
}
[Do something with the image]
}
})
})
})
}
UPDATE: Not sure why Jason is sticking this answer all over the net. IT DOES NOT solve the problem of HOW to Screenshot the Full WKWebView content... including that off the screen.
Try this:
func snapshot() -> UIImage {
UIGraphicsBeginImageContextWithOptions(self.webView.bounds.size, true, 0);
self.webView.drawViewHierarchyInRect(self.webView.bounds, afterScreenUpdates: true);
let snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageWriteToSavedPhotosAlbum(snapshotImage, nil, nil, nil)
return snapshotImage
}
The image will automatically save into the iOS Camera Roll (by UIImageWriteToSavedPhotosAlbum()).

Having an issue animating certain elements UIView will excluding others

In my code, I've correctly moved the UITextfield into view when the keyboard pops up. However, I've struggled to create another animation that acts only on the UIImageView as this current solution permanently offsets the logo in the UIImageView.
var logoImage:UIImageView!
var logoImageX:CGFloat = 85
var logoImageY:CGFloat = 35
// Logo Code
let logo:UIImage = UIImage(named:"color-logo.png")
let logoHeight:CGFloat = 150
let logoWidth:CGFloat = logoHeight
logoImage = UIImageView(image:Logo)
logoImage.frame = CGRectMake(logoImageX,logoImageY,logoWidth, logoHeight)
self.view.insertSubview(logoImage, atIndex: 1)
// Keyboard Auto Scroll
func textFieldDidBeginEditing(textField: UITextField!) {
self.animateTextField(textField, up: true)
self.animateWithDuration(true)
}
func textFieldDidEndEditing(textField: UITextField!) {
self.animateTextField(textField, up: false)
self.animateWithDuration(false)
}
func animateTextField(textField:UITextField,up: Bool){
let movementDistance:Int = -100
let movementDuration:NSTimeInterval = 0.25
var movement = CGFloat(Int((up ? movementDistance : -movementDistance)))
UIView.beginAnimations("animateTextField", context:nil)
UIView.setAnimationBeginsFromCurrentState(true)
UIView.setAnimationDuration(movementDuration)
self.view.frame = CGRectOffset(self.view.frame, 0, movement)
UIView.commitAnimations()
}
Here's the function for the UIImageView:
//Logo (UIImageView) Scroll
func animateWithDuration(up:Bool){
let logoMovementDuration = 0.25
let logoMovementDistance:Int = 20
var logoMovement = CGFloat(Int((up ? logoMovementDistance : -logoMovementDistance + (logoImageY))))
UIView.beginAnimations("animateWithDuration", context:nil)
UIView.setAnimationDuration(logoMovementDuration)
logoImage.frame = CGRectMake(logoImageX,logoMovement,logoImage.frame.size.width,logoImage.frame.size.height)
UIView.commitAnimations()
}
I've found a solution by creating multiple UIViews, or in my case 3 specifically.
The code looks like this, depending on what you want to achieve, segregated into different groups in order to apply animations to each of them (these are declared outside of the viewDidLoad() to make them publicly accessible):
var otherView = UIView(frame: UIScreen.mainScreen().bounds)
var logoImageView = UIView(frame:CGRect(x: 0, y: 0, width: xx, height: xx))
var resultView = UIView(frame:CGRect(x: 0, y: 0, width: xx, height: xx))
After which, I added the subviews to UIView instances and then finally into the super view (declared within the viewDidLoad()):
self.logoImageView.insertSubview(logoImage, atIndex:1)
self.view.insertSubview(logoImageView, atIndex: 1)
Then finally applied the same func's now using the block based animations as suggested by rdelmar and recommended by Apple since iOS 4.0 (outside of, and after, the viewDidLoad()):
// Keyboard Auto Scroll
func textFieldDidBeginEditing(textField: UITextField!) {
self.animateTextField(textField, up: true)
}
func textFieldDidEndEditing(textField: UITextField!) {
self.animateTextField(textField, up: false)
}
func animateTextField(textField:UITextField,up: Bool){
let resultMovementDistance:Int = -xx
let movementDistance:Int = -xx
let movementDuration:NSTimeInterval = 0.xx
var resultMovement = CGFloat(Int((up ? resultMovementDistance : -resultMovementDistance)))
var otherMovement = CGFloat(Int((up ? movementDistance : -movementDistance)))
var logoMovement = otherMovement/5
UIView.animateWithDuration(
movementDuration,
delay: 0,
options: UIViewAnimationOptions.CurveEaseIn, animations:{
self.otherView.frame = CGRectOffset(self.otherView.frame,0,otherMovement)},
completion:nil)
UIView.animateWithDuration(
movementDuration,
delay: 0,
options: UIViewAnimationOptions.CurveEaseIn, animations:{
self.logoImageView.frame = CGRectOffset(self.logoImageView.frame,0,logoMovement)},
completion:nil)
UIView.animateWithDuration(
movementDuration,
delay: 0,
options: UIViewAnimationOptions.CurveEaseIn, animations:{
self.resultView.frame = CGRectOffset(self.resultView.frame,0,resultMovement)},
completion:nil)
}
Note: I did struggle with nesting the animations in the block, but I'm glad that it worked out. I hope this helps.

Resources