I am creating an application with a fixed background (2208x2208) for every screen, that is why I am setting the background in the application delegate. The current code inside the didFinishLaunchingWithOptions method is:
func setBackground() {
let width = self.window!.frame.size.width// * UIScreen.mainScreen().nativeScale
let height = self.window!.frame.size.height// * UIScreen.mainScreen().nativeScale
let size = max(width, height)
UIGraphicsBeginImageContext(CGSize(width: width, height: height))
UIImage(named: "background.jpg")?.drawInRect(CGRectMake(-(size - width) / 2, -(size - height) / 2, size, size))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.window?.backgroundColor = UIColor(patternImage: image!)
}
This works like it supposes, but with one issue. The image looks like it is scaled down and scaled back to the scale of the screen. So on an iPhone 6 Plus it doesn't look sharp. This might be expected behaviour since the window size is not the full resolution. But when I multiply it with the native scale, only 1/9th the image (upper left) is shown. Is there a way (besides providing separate resolutions for this image) to show it without scaling first?
A second issue, for if anyone know the solution for this. The image is shown behind a UINavigationController where every view inside the UIViewController has a transparent background. This works and shows the background, but one problem, the background turns a little darker when navigation to another view. Probably because the animation. Is there a way to hide this animation? Inside the UIViewControllers (there are only two yet, but this will be inside a class which will be overridden) is the following code:
override func viewWillAppear(animated: Bool) {
self.view.alpha = 1.0
super.viewWillAppear(animated)
}
override func viewWillDisappear(animated: Bool) {
UIView.animateWithDuration(0.25, animations: {
self.view.alpha = 0.0
})
super.viewWillDisappear(animated)
}
Even without this code, it still turns darker before it goes light again. It looks like the view still as a dark opaque background with a very low alpha and when presenting the second screen, it lays both backgrounds on top of each other until the second screen is completely presented and the first one will be removed.
Thanks!
Related
I have a code that creates a new UIImageView everytime a button is clicked and that UIImageView is then animated. However, whenever the button is clicked after it is clicked for the first time, I think the animation applied to the second UIImageView that is created affects the first UIImageView. In other words, both start to move very fast (much faster that programmed). This is my code:
#IBAction func buttonClicked(_ sender: Any) {
var imageName: String = "ImageName"
var image = UIImage(named: imageName)
var imageView = UIImageView(image: image!)
imageView.frame = CGRect(x: 0,y: 0,width: 50,height: 50)
view.addSubView(imageView)
moveIt(imageView)
}
func moveIt(_ imageView: UIImageView) {
UIView.animate(withDuration:TimeInterval(0.00001), delay: 0.0, options: .curveLinear, animations: {
imageView.center.x+=3
}, completion: {(_) in self.moveIt(imageView)
})
}
I am relatively new with code and swift. Thanks in advance because I cannot quite seem to figure this out.
You should not try to do a whole bunch of chained animations every 0.00001 seconds. Most things on iOS have a time resolution of ≈ .02 seconds.
You are trying to run an animation every 10 microseconds and move the image view 10 points each step. That comes to an animation of a million points per second. Don't do that.
Just create a single animation that moves the image view to it's final destination over your desired time-span. The system will take care of pacing the animation for you. (Say you move it 100 pixels in 0.3 seconds. Just specify a duration of 0.3 seconds, and the body of the animation moves the image view by 100 pixels. There you go. It will just work.
Note that your image views don't have any auto-layout constraints, so they will likely act strangely once the animation is complete.
I'm working on a feature that will animate a UIView up from the bottom of the screen to tell the user their data was saved. However, it doesn't appear in the same place on an iPhone 11 Pro Max as it does on an iPhone X.
iPhone 11 Pro Max (this is what I want it to look like):
but on an iPhone X:
I should probably note that I created the view on the storyboard and have an #IBOutlet connected to it. On the storyboard, it's positioned where you see it in the top screenshot. I did not place any constraints on it. The rest is done in code.
The code to animate is very simple:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
savedView.center.y += view.bounds.height // start with it off the screen
savedView.center.x = (view.bounds.width / 2)
}
override func viewDidAppear(_ animated: Bool) {
super.viewWillAppear(true)
// animate it onto the screen
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, options: [], animations: {
self.savedView.center.y -= self.view.bounds.height <-- the problem must be here
}, completion: nil)
}
I thought it might be due to different device size classes but all iPhones are 'regular' height so I don't think that's it.
How can I get this view to end up in the same position (relative to the bottom of the screen) on all devices?
In order to place your views in the same place across all devices is a must to have constraints set up. So that, the system is able to show your views correctly.
So to achieve a bottom-centered attached behavior for your view, you need to set up a constraint to the safe-area bottom, a constraint called centered-horizontally, width and height, assuming that this view is the only component on screen. If it is not, then there must be a top constraint for the view with the component in the top of it.
Hope this helps, at least to give you an idea.
Constraints reference
I was able to figure it out.
The problem was that the view's initial center.y position was starting off at 814 based on its position in the storyboard. So it looked good on the storyboard which I'm viewing as an iPhone 11 Pro but, when run on other device simulators with different dimensions, the math didn't work anymore.
The solution was to set the center.y position based on the view.bounds.height giving it a consistent starting position on any device:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
savedView.center.y = (view.bounds.height - 60) // position the view 60 pixels bottom
savedView.center.x = (view.bounds.width / 2) // center it horizontally
savedView.center.y += view.bounds.height // move it outside the visible bounds
}
I use the following code to scroll to top of the UICollectionView:
scrollView.scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: true)
However, on iOS 11 and 12 the scrollView only scrolls to the top, without revealing the large title of the UINavigationBar (when prefersLargeTitle has ben set to true.)
Here is how it looks like:
The result I want to achieve:
It works as it is designed, you are scrolling to position y = 0, assign your controller to be UIScrollView delegate and print out scroll offset:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset)
}
You will see when Large title is displayed and you move your scroll view a but and it jumps back to the Large title it will not print (0.0, 0.0) but (0.0, -64.0) or (0.0, -116.0) - this is the same value as scrollView.adjustedContentInset, so if you want to scroll up and display large title you should do:
scrollView.scrollRectToVisible(CGRect(x: 0, y: -64, width: 1, height: 1), animated: true)
You don't want to use any 'magic values' (as -64 in the currently accepted answer). These may change (also, -64 isn't correct anyway).
A better solution is to observe the SafeAreaInsets changes and save the biggest top inset. Then use this value in the setContentOffset method. Like this:
class CollectioViewController: UIViewController {
var biggestTopSafeAreaInset: CGFloat = 0
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
self.biggestTopSafeAreaInset = max(ui.safeAreaInsets.top, biggestTopSafeAreaInset)
}
func scrollToTop(animated: Bool) {
ui.scrollView.setContentOffset(CGPoint(x: 0, y: -biggestTopSafeAreaInset), animated: animated)
}
}
It seems that using a negative content offset is the way to go.
I really like the idea of Demosthese to keep track of the biggest top inset.
However, there is a problem with this approach.
Sometime large titles cannot be displayed, for example, when an iPhone is in landscape mode.
If this method is used after a device has been rotated to landscape then the offset of the table will be too much because the large title is not displayed in the navigation bar.
An improvements to this technique is to consider biggestTopSafeAreaInset only when the navigation bar can display a large title.
Now the problem is to understand when a navigation bar can display a large title.
I did some test on different devices and it seems that large titles are not displayed when the vertical size class is compact.
So, Demosthese solution can be improved in this way:
class TableViewController: UITableViewController {
var biggestTopSafeAreaInset: CGFloat = 0
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
self.biggestTopSafeAreaInset = max(view.safeAreaInsets.top, biggestTopSafeAreaInset)
}
func scrollToTop(animated: Bool) {
if traitCollection.verticalSizeClass == .compact {
tableView.setContentOffset(CGPoint(x: 0, y: -view.safeAreaInsets.top), animated: animated)
} else {
tableView.setContentOffset(CGPoint(x: 0, y: -biggestTopSafeAreaInset), animated: animated)
}
}
}
There is still a case that could cause the large title to not be displayed after the scroll.
If the user:
Open the app with the device rotated in landscape mode.
Scroll the view.
Rotate the device in portrait.
At this point biggestTopSafeAreaInset has not yet had a chance to find the greatest value and if the scrollToTop method is called the large title will be not displayed.
Fortunately, this is a case that shouldn't happen often.
Quite late here but I have my version of the story.
Since iOS 11 there is the adjustedContentInset on the scroll view.
That however reflects only the current state of the UI thus if the large navigation title is not revealed, it won't be taken into account.
So my solution is to make couple of extra calls to make the system consider the large title size and calculate it to the adjustedContentInset:
extension UIScrollView {
func scrollToTop(animated: Bool = true) {
if animated {
// 1
let currentOffset = contentOffset
// 2
setContentOffset(CGPoint(x: 0, y: -adjustedContentInset.top - 1), animated: false)
// 3
let newAdjustedContentInset = adjustedContentInset
// 4
setContentOffset(currentOffset, animated: false)
// 5
setContentOffset(CGPoint(x: 0, y: -newAdjustedContentInset.top), animated: true)
} else {
// 1
setContentOffset(CGPoint(x: 0, y: -adjustedContentInset.top - 1), animated: false)
// 2
setContentOffset(CGPoint(x: 0, y: -adjustedContentInset.top), animated: false)
}
}
}
Here is what's happening:
When animated:
Get the current offset to be able to apply it again (important for achieving the animation)
Scroll without animating to the currently calculated adjustedContentInset plus some more because the large title was not considered when calculating the adjustedContentInset
Now the system takes into account the large title so get the current adjustedContentInset that will include its size so store it to a constant that will be used in the last step
Scroll back to the original offset without animating so no visual changes will be noticed
Scroll to the previously calculated adjustedContentInset this time animating to achieve the desired animated scrolling
When !animated:
Scroll without animation to the adjustedContentInset plus some more. At this stage the system will consider the large title so...
Scroll to the current adjustedContentInset as it was calculated with the large title in it
Kind of a hack but does work.
I have a horizontal UIScrollview in my app which has 1 really long UIImageView to start with. I have a timer and animation to create an illusion that the image under scroll view is automatically scrolling. Once the image comes to an end i dynamically add similar image to the scroll view so it should look like the image is repeating itself.
This is how i want them to be displayed under scroll view : image1|image2|image3|image4...... and these images will be scrolling from right to left. Exactly how it works in Behance's iphone app before you login.
Here's the code i have (in storyboard i have the scroll view and one UIIMageview already added).
override func viewDidAppear(_ animated: Bool) {
Timer.scheduledTimer(timeInterval: 0.6, target: self, selector: #selector(scrollImage), userInfo: nil, repeats: true)
}
#objc func scrollImage() {
offSet.x = offSet.x + CGFloat(20)
UIView.animate(withDuration: 1.0, animations: {
self.behanceView.setContentOffset(self.offSet, animated: false)
})
}
func addImagetoScrollView() {
let imageView = UIImageView(image:UIImage(named:"Landing_Scrollable"))
print(imageCount*Int(self.behanceView.contentSize.width)+100)
imageView.frame = CGRect(x:imageCount*Int(self.behanceView.contentSize.width), y: 0, width: 875, height: 502)
self.behanceView.contentSize = CGSize(width: imageView.bounds.size.width * CGFloat(imageCount), height: imageView.bounds.size.height)
self.behanceView.addSubview(imageView)
imageCount+=1
}
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let scrollViewWidth = scrollView.frame.size.width;
let scrollOffset = scrollView.contentOffset.x;
print(imageCount*Int(self.behanceView.contentSize.width - scrollViewWidth))
if scrollOffset >= CGFloat(imageCount*Int(self.behanceView.contentSize.width - scrollViewWidth)) {
self.addImagetoScrollView()
}
}
}
But when i see it in action, it does something wierd and animation is all off.
Can someone please help.
Thanks,
I've never seen the “Behance” app, but I guess you're asking how to animate a seamlessly tiled background image across the screen indefinitely, like this:
(Pattern image by Evan Eckard.)
I used an animation duration of 1 second for the demo, but you probably want a much longer duration in a real app.
You shouldn't use a timer for this. Core Animation can perform the animation for you, and letting it perform the animation smoother and more efficient. (You might think Core Animation is performing your animation since you're using UIView animation, but I believe animating a scroll view's contentOffset does not use Core Animation because the scroll view has to call its delegate's scrollViewDidScroll on every animation step.)
You also shouldn't use a scroll view for this. UIScrollView exists to allow the user to scroll. Since you're not letting the user scroll, you shouldn't use UIScrollView.
Here's how you should set up your background:
Create two identical image views (numbered 0 and 1), showing the same image. Make sure the image views are each big enough to fill the screen.
Put the left edge of image view 0 at the left edge of your root view. Put the left edge of image view 1 at the right edge of image view 0. Since each image view is big enough to fill the screen, image view 1 will start out entirely off the right edge of the screen.
Animate image view 0's transform.translation.x from 0 to -imageView.bounds.size.width. This will make it slide to the left by precisely its own width, so when the animation reaches its end, image view 0's right edge is at the left edge of the screen (and thus image view 0 is entirely off the left edge of the screen). Set the animation's repeatCount to .infinity.
Add the same animation to image view 1. Thus image view 1 comes onto the screen as image view 0 is leaving it, exactly covering the pixels revealed by image view 0's animation.
The two animations end at exactly the same time. When they end, image view 1 is exactly where image view 0 was at the start. Since both animations are set to repeat infinitely, they both immediately start over. When image view 0's animation starts over, image view 0 instantly jumps back to its starting position, which is where image view 1 ended up. Since both image views show the same image, the pixels on screen don't change. This makes the animation loop seamless.
Here's my code:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
for imageView in imageViews {
imageView.image = patternImage
imageView.contentMode = .scaleToFill
view.addSubview(imageView)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let bounds = view.bounds
let patternSize = patternImage.size
// Scale the image up if necessary to be at least as big as the screen on both axes.
// But make sure scale is at least 1 so I don't shrink the image if it's larger than the screen.
let scale = max(1 as CGFloat, bounds.size.width / patternSize.width, bounds.size.height / patternSize.height)
let imageFrame = CGRect(x: 0, y: 0, width: scale * patternSize.width, height: scale * patternSize.height)
for (i, imageView) in imageViews.enumerated() {
imageView.frame = imageFrame.offsetBy(dx: CGFloat(i) * imageFrame.size.width, dy: 0)
let animation = CABasicAnimation(keyPath: "transform.translation.x")
animation.fromValue = 0
animation.toValue = -imageFrame.size.width
animation.duration = 1
animation.repeatCount = .infinity
animation.timingFunction = CAMediaTimingFunction(name: .linear)
// The following line prevents iOS from removing the animation when the app goes to the background.
animation.isRemovedOnCompletion = false
imageView.layer.add(animation, forKey: animation.keyPath)
}
}
private let imageViews: [UIImageView] = [.init(), .init()]
private let patternImage = #imageLiteral(resourceName: "pattern")
}
I am trying to recreate the effect that YouTube mobile app on iOS has, when a video is playing in full-screen, landscape mode. Just the top part of a window (next/recommended videos) is visible, and swiping up shows them overlapped with video (that keeps running in background). Swiping down hides them again.
So I added the following code within my video controller that shows the video in landscape mode:
private func showPopover() {
let popoverController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "popoverController")
popoverController.modalPresentationStyle = .popover
popoverController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
popoverController.popoverPresentationController?.delegate = self
let desiredWidth:CGFloat = 200
let desiredHeight:CGFloat = 300
// Initially it's at low right corner
let origin = CGPoint(x: (self.view.superview?.frame.width)! - desiredWidth - 10, y: (self.view.superview?.frame.height)! - 10)
let size = CGSize(width: desiredWidth, height: desiredHeight)
popoverController.popoverPresentationController?.sourceRect = CGRect(origin: origin, size: size)
popoverController.popoverPresentationController?.sourceView = popoverController.view
self.present(popoverController, animated: true, completion: nil)
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
The popover shows up fine, but is always completely on screen. I need it to be only partially visible (just the top 10 pixels, if possible). But something is preventing the view controller from being positioned that way.
How do I move the popover so that only top 10 pixels are visible? It should look something like in the image below (note: this was generating by editing image, not programmatically):
Replace this:
popoverController.popoverPresentationController?.sourceView = popoverController.view
with this:
popoverController.popoverPresentationController?.sourceView = self.view
UPDATE
Ok I understand now. My answer would be it is not possible to only show some part of the popover.
My suggestion here is using UITableViewController as your popover and making its height just enough to show the first row, which is the text in this case.
Then, in optional func scrollViewDidScroll(_ scrollView: UIScrollView) delegate method, implement logics to detect swiping up to show and swiping down to hide.
Found a way, and it's ridiculously easy (with hindsight, of course!). Changed the last line of showPopover function to:
self.present(popoverController, animated: true) {
popoverController.popoverPresentationController?.presentedView?.center = CGPoint(x: (self.view.superview?.frame.width)! - desiredWidth - 10, y: (self.view.superview?.frame.height)! + 100)
}
I will get a specific number instead of using 100 but the view moves to this position without any issues.