I am trying to implement custom activity indicator which is simple rotating UIImageView. I have next code
import Foundation
import UIKit
class CatSpinnerAnimation: UIView {
let catImageView: UIImageView = {
let catImageView = UIImageView()
catImageView.image = UIImage(named: "CatDateIcon-1024")
catImageView.translatesAutoresizingMaskIntoConstraints = false
return catImageView
}()
func startRotating(superView: UIView) {
superView.addSubview(catImageView)
UIApplication.shared.beginIgnoringInteractionEvents()
catImageView.centerXAnchor.constraint(equalTo: superView.centerXAnchor).isActive = true
catImageView.centerYAnchor.constraint(equalTo: superView.centerYAnchor).isActive = true
catImageView.widthAnchor.constraint(equalToConstant: 50).isActive = true
catImageView.heightAnchor.constraint(equalToConstant: 50).isActive = true
let rotate = CGAffineTransform(rotationAngle: CGFloat.pi)
UIView.animate(withDuration: 5.0) {
self.catImageView.transform = rotate
}
}
func stopRotating(superView: UIView) {
UIApplication.shared.endIgnoringInteractionEvents()
catImageView.removeFromSuperview()
}
}
In my ViewControllers I instantiate an instance of this class and call its function
let catSpinnerAnimation = CatSpinnerAnimation()
catSpinnerAnimation.startRotating(superView: view)
But this doesn't show rotating animation at all, just my ImageView in final position (rotated for 189 degrees). I tried many types of animation but still don't have necessary result. I would appreciate any explanation of correct behavior of rotating animations
I think you might need to let the view be added to the view hierarchy first before you can animate it. Try wrapping your call to UIView.animate() in a call to DispatchQueue.main.asyncAfter(deadline: .now() + .01) {}
That would let your view be added to the view hierarchy and get rendered before you submit your animation.
Related
I am trying to add alpha to the background view when tapped on a button. So far achieved adding blur but alpha not so much.
How can I add alpha to the background so that when the bottom sheet appears background will be darker and disabled.
let maxDimmedAlpha: CGFloat = 0.2
lazy var dimmedView: UIView = {
let view = UIView()
view.backgroundColor = .black
view.alpha = maxDimmedAlpha
return view
}()
#objc func shareBtnClick() {
dimmedView.frame = self.parentVC.view.bounds
dimmedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.parentVC.view.addSubview(dimmedView)
if self.parentVC.navigationController != nil {
if self.parentVC.navigationController?.viewControllers.count == 1 {
showBottomSheet()
} else {
NotificationCenter.default.post(name: NSNotification.Name("ShowBottomSheet"), object: nil, userInfo: ["itemId": modalSheet(), "delegate": self])
}
} else {
showBottomSheet()
}
}
func showBottomSheet() {
let modalSheet = MainBottomSheet()
modalSheet.data = self.modalSheet()
modalSheet.delegate = self
modalSheet.modalPresentationStyle = .overCurrentContext
self.parentVC.present(modalSheet, animated: true)
}
I was able to produce the dimmed effect using this code in XCode, I'm not sure why it won't work in your project but there is an easy way to debug this.
I suggest using Debug View Hierarchy, one of XCode's best tools in my opinion. This allows you to separate every single layer of the user interface. This way, you can see if your dimmedView is actually being added to the parent view and that its frame is matching the parent view's bounds.
Keep in mind if your background is dark, you won't see this dimmedView because its backgroundColor is set to UIColor.black.
Debug View Hierarchy button
(Swift, macOS, storyboard)
I have an NSView in a transparent window
I have this in the viewDidLoad. To make the window transparent and the NSView blue:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2){
self.view.window?.isOpaque = false
self.view.window?.backgroundColor = NSColor.clear
}
view1.wantsLayer = true
view1.layer?.backgroundColor = NSColor.green.cgColor
I want to change the width with code when I click a button.
If it has constraints:
#IBAction func button1(_ sender: NSButton) {
view1Width.constant = 74
}
I tried without constraints and different ways to change the width. They all give the same results:
view1.frame = NSRect(x:50, y:120, width:74, height:100)
But there is still a border and a shadow where the old shape was. Why does it happen and how to solve it?
It only happens in specific circumstances:
If the window is transparent (and macOS)
I change the width and do not change the position y
The window must be active. If it is not (If I click to anywhere else) it looks as it should: the shadow around the changed NSView green.
(I have simplified the case to try to find a solution. I have created a new document and there is only this code and I am sure there is no other element)
Since the window is transparent you need to invalidate the shadows.
Apple states about invalidateShadow()
Invalidates the window shadow so that it is recomputed based on the current window shape.
Complete Self-Contained Test Program
It sets up the UI pogrammatically instead of using a storyboard. Other than that, the code is very close to your example.
Note the line:
view.window?.invalidateShadow()
in the onChange method.
import Cocoa
class ViewController: NSViewController {
private let view1 = NSView()
private let changeButton = NSButton()
private var view1Width: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2){
self.view.window?.isOpaque = false
self.view.window?.backgroundColor = NSColor.clear
}
view1.wantsLayer = true
view1.layer?.backgroundColor = NSColor.green.cgColor
}
#objc private func onChange() {
view1Width?.constant += 32
view.window?.invalidateShadow()
}
private func setupUI() {
changeButton.title = "change"
changeButton.bezelStyle = .rounded
changeButton.setButtonType(.momentaryPushIn)
changeButton.target = self
changeButton.action = #selector(onChange)
self.view.addSubview(view1)
self.view.addSubview(changeButton)
self.view1.translatesAutoresizingMaskIntoConstraints = false
self.changeButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
view1.centerXAnchor.constraint(equalTo: view.centerXAnchor),
view1.centerYAnchor.constraint(equalTo: view.centerYAnchor),
view1.heightAnchor.constraint(equalToConstant: 128),
changeButton.topAnchor.constraint(equalTo: view1.bottomAnchor, constant:16),
changeButton.centerXAnchor.constraint(equalTo: view1.centerXAnchor)
])
view1Width = view1.widthAnchor.constraint(equalToConstant: 128)
view1Width?.isActive = true
}
}
Result
The desired result with an update of the shadows is accomplished:
FirebaseUI has a nice pre-buit UI for Swift. I'm trying to position an image view above the login buttons on the bottom. In the example below, the imageView is the "Hackathon" logo. Any logo should be able to show in this, if it's called "logo", since this shows the image as aspectFit.
According to the Firebase docs page:
https://firebase.google.com/docs/auth/ios/firebaseui
You can customize the signin screen with this function:
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
return FUICustomAuthPickerViewController(nibName: "FUICustomAuthPickerViewController",
bundle: Bundle.main,
authUI: authUI)
}
Using this code & poking around with subviews in the debuggers, I've been able to identify and color code views in the image below. Unfortunately, I don't think that the "true" size of these subview frames is set until the view controller presents, so trying to access the frame size inside these functions won't give me dimensions that I can use for creating a new imageView to hold a log. Plus accessing the views with hard-coded index values like I've done below, seems like a pretty bad idea, esp. given that Google has already changed the Pre-Built UI once, adding a scroll view & breaking the code of anyone who set the pre-built UI's background color.
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
// Create an instance of the FirebaseAuth login view controller
let loginViewController = FUIAuthPickerViewController(authUI: authUI)
// Set background color to white
loginViewController.view.backgroundColor = UIColor.white
loginViewController.view.subviews[0].backgroundColor = UIColor.blue
loginViewController.view.subviews[0].subviews[0].backgroundColor = UIColor.red
loginViewController.view.subviews[0].subviews[0].tag = 999
return loginViewController
}
I did get this to work by adding a tag (999), then in the completion handler when presenting the loginViewController I hunt down tag 999 and call a function to add an imageView with a logo:
present(loginViewController, animated: true) {
if let foundView = loginViewController.view.viewWithTag(999) {
let height = foundView.frame.height
print("FOUND HEIGHT: \(height)")
self.addLogo(loginViewController: loginViewController, height: height)
}
}
func addLogo(loginViewController: UINavigationController, height: CGFloat) {
let logoFrame = CGRect(x: 0 + logoInsets, y: self.view.safeAreaInsets.top + logoInsets, width: loginViewController.view.frame.width - (logoInsets * 2), height: self.view.frame.height - height - (logoInsets * 2))
// Create the UIImageView using the frame created above & add the "logo" image
let logoImageView = UIImageView(frame: logoFrame)
logoImageView.image = UIImage(named: "logo")
logoImageView.contentMode = .scaleAspectFit // Set imageView to Aspect Fit
// loginViewController.view.addSubview(logoImageView) // Add ImageView to the login controller's main view
loginViewController.view.addSubview(logoImageView)
}
But again, this doesn't seem safe. Is there a "safe" way to deconstruct this UI to identify the size of this button box at the bottom of the view controller (this size will vary if there are multiple login methods supported, such as Facebook, Apple, E-mail)? If I can do that in a way that avoids the hard-coding approach, above, then I think I can reliably use the dimensions of this button box to determine how much space is left in the rest of the view controller when adding an appropriately sized ImageView. Thanks!
John
This should address the issue - allowing a logo to be reliably placed above the prebuilt UI login buttons buttons + avoiding hard-coding the index values or subview locations. It should also allow for properly setting background color (also complicated when Firebase added the scroll view + login button subview).
To use: Create a subclass of FUIAuthDelegate to hold a custom view controller for the prebuilt Firebase UI.
The code will show the logo at full screen behind the buttons if there isn't a scroll view or if the class's private constant fullScreenLogo is set to false.
If both of these conditions aren't meant, the logo will show inset taking into account the class's private logoInsets constant and the safeAreaInsets. The scrollView views are set to clear so that a background image can be set, as well via the private let backgroundColor.
Call it in any signIn function you might have, after setting authUI.providers. Call would be something like this:
let loginViewController = CustomLoginScreen(authUI: authUI!)
let loginNavigationController = UINavigationController(rootViewController: loginViewController)
loginNavigationController.modalPresentationStyle = .fullScreen
present(loginNavigationController, animated: true, completion: nil)
And here's one version of the subclass:
class CustomLoginScreen: FUIAuthPickerViewController {
private var fullScreenLogo = false // false if you want logo just above login buttons
private var viewContainsButton = false
private var buttonViewHeight: CGFloat = 0.0
private let logoInsets: CGFloat = 16
private let backgroundColor = UIColor.white
private var scrollView: UIScrollView?
private var viewContainingButton: UIView?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// set color of scrollView and Button view inside scrollView to clear in viewWillAppear to avoid a "color flash" when the pre-built login UI first appears
self.view.backgroundColor = UIColor.white
guard let foundScrollView = returnScrollView() else {
print("😡 Couldn't get a scrollView.")
return
}
scrollView = foundScrollView
scrollView!.backgroundColor = UIColor.clear
guard let foundViewContainingButton = returnButtonView() else {
print("😡 No views in the scrollView contain buttons.")
return
}
viewContainingButton = foundViewContainingButton
viewContainingButton!.backgroundColor = UIColor.clear
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Create the UIImageView at full screen, considering logoInsets + safeAreaInsets
let x = logoInsets
let y = view.safeAreaInsets.top + logoInsets
let width = view.frame.width - (logoInsets * 2)
let height = view.frame.height - (view.safeAreaInsets.top + view.safeAreaInsets.bottom + (logoInsets * 2))
var frame = CGRect(x: x, y: y, width: width, height: height)
let logoImageView = UIImageView(frame: frame)
logoImageView.image = UIImage(named: "logo")
logoImageView.contentMode = .scaleAspectFit // Set imageView to Aspect Fit
logoImageView.alpha = 0.0
// Only proceed with customizing the pre-built UI if you found a scrollView or you don't want a full-screen logo.
guard scrollView != nil && !fullScreenLogo else {
print("No scrollView found.")
UIView.animate(withDuration: 0.25, animations: {logoImageView.alpha = 1.0})
self.view.addSubview(logoImageView)
self.view.sendSubviewToBack(logoImageView) // otherwise logo is on top of buttons
return
}
// update the logoImageView's frame height to subtract the height of the subview containing buttons. This way the buttons won't be on top of the logoImageView
frame = CGRect(x: x, y: y, width: width, height: height - (viewContainingButton?.frame.height ?? 0.0))
logoImageView.frame = frame
self.view.addSubview(logoImageView)
UIView.animate(withDuration: 0.25, animations: {logoImageView.alpha = 1.0})
}
private func returnScrollView() -> UIScrollView? {
var scrollViewToReturn: UIScrollView?
if self.view.subviews.count > 0 {
for subview in self.view.subviews {
if subview is UIScrollView {
scrollViewToReturn = subview as? UIScrollView
}
}
}
return scrollViewToReturn
}
private func returnButtonView() -> UIView? {
var viewContainingButton: UIView?
for view in scrollView!.subviews {
viewHasButton(view)
if viewContainsButton {
viewContainingButton = view
break
}
}
return viewContainingButton
}
private func viewHasButton(_ view: UIView) {
if view is UIButton {
viewContainsButton = true
} else if view.subviews.count > 0 {
view.subviews.forEach({viewHasButton($0)})
}
}
}
Hope this helps any who have been frustrated trying to configure the Firebase pre-built UI in Swift.
I want to create an animation like the iOS app facebook at tabswitch[1]. I have already tried to develop some kind of animation, the problem that occurs is that the old view controller becomes invisible directly on the switch, instead of fading out slowly while the new controller is sliding in fast.
I've found this SO question How to animate Tab bar tab switch with a CrossDissolve slide transition? but the as correct marked solution does not really work for me (it is not a slide it is a fade transition). What I'd also like to get is the function to make slide left or right to switch the tabs. Like it was on a older version of facebook.
What I've got so far is this:
extension TabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let fromView = selectedViewController?.view,
let toView = viewController.view else { return false }
if fromView != toView {
toView.transform = CGAffineTransform(translationX: -90, y: 0)
UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseInOut, animations: {
toView.transform = CGAffineTransform(translationX: 0, y: 0)
})
}; return true
}
}
class TabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
How to fix this?
[1]
I would very much like to add a gif from the Facebook app. The problem is that I don't want to censor the video and just reveal too much of my data. (Even if fb already has them). Also on youtube I didn't find a suitable recording. Please try it yourself in the fb app in iOS.
You can use the following idea: https://samwize.com/2016/04/27/making-tab-bar-slide-when-selected/
Also, here's the code updated to Swift 4.1 and I also removed the force unwrappings:
import UIKit
class MyTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
extension MyTabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let tabViewControllers = tabBarController.viewControllers, let toIndex = tabViewControllers.index(of: viewController) else {
return false
}
animateToTab(toIndex: toIndex)
return true
}
func animateToTab(toIndex: Int) {
guard let tabViewControllers = viewControllers,
let selectedVC = selectedViewController else { return }
guard let fromView = selectedVC.view,
let toView = tabViewControllers[toIndex].view,
let fromIndex = tabViewControllers.index(of: selectedVC),
fromIndex != toIndex else { return }
// Add the toView to the tab bar view
fromView.superview?.addSubview(toView)
// Position toView off screen (to the left/right of fromView)
let screenWidth = UIScreen.main.bounds.size.width
let scrollRight = toIndex > fromIndex
let offset = (scrollRight ? screenWidth : -screenWidth)
toView.center = CGPoint(x: fromView.center.x + offset, y: toView.center.y)
// Disable interaction during animation
view.isUserInteractionEnabled = false
UIView.animate(withDuration: 0.3,
delay: 0.0,
usingSpringWithDamping: 1,
initialSpringVelocity: 0,
options: .curveEaseOut,
animations: {
// Slide the views by -offset
fromView.center = CGPoint(x: fromView.center.x - offset, y: fromView.center.y)
toView.center = CGPoint(x: toView.center.x - offset, y: toView.center.y)
}, completion: { finished in
// Remove the old view from the tabbar view.
fromView.removeFromSuperview()
self.selectedIndex = toIndex
self.view.isUserInteractionEnabled = true
})
}
}
So, you need to subclass UITabBarController and you also have to write the animation part, you can tweak the animation options (delay, duration, etc).
I hope it helps, cheers!
I've never seen Facebook so I don't know what the animation is. But you can have any animation you like when a tab bar controller changes its tab (child view controller), coherently and without any hacks, using the built-in mechanism that Apple provides for adding custom animation to a transition between view controllers. It's called custom transition animation.
Apple first introduced this mechanism in 2013. Here's a link to their video about it: https://developer.apple.com/videos/play/wwdc2013/218/
I immediately adopted this in my apps, and I think it makes them look a lot spiffier. Here's a demo of a tab bar controller custom transition that I like:
The really cool thing is that once you've decided what animation you want, making the transition interactive (i.e. drive it with a gesture instead of a button click) is easy:
Now, you might be saying: Okay, but that's not quite the animation I had in mind. No problem! Once you've got the hang of the custom transition architecture, changing the animation to anything you like is easy. In this variant, I just commented out one line so that the "old" view controller doesn't slide away:
So let your imagination run wild! Adopt custom transition animations, the way that iOS intends.
If you want something for pushViewController navigation, you can try this.
However, when switching between tabs on a TabBarController, this will not work. For that, I'd go with #mihai-erős 's solution
Change the Animation duration as per your liking, and assign this class to your navigation segues, for a Slide Animation.
class CustomPushSegue: UIStoryboardSegue {
override func perform() {
// first get the source and destination view controllers as UIviews so that they can placed in navigation stack
let sourceVCView = self.source.view as UIView!
let destinationVCView = self.destination.view as UIView!
let screenWidth = UIScreen.main.bounds.size.width
//create the destination view's rectangular frame i.e starting at 0,0 and equal to screenwidth by screenheight
destinationVCView?.transform = CGAffineTransform(translationX: screenWidth, y: 0)
//the destinationview needs to be placed on top(aboveSubView) of source view in the app window stack before being accessed by nav stack
// get the window and insert destination View
let window = UIApplication.shared.keyWindow
window?.insertSubview(destinationVCView!, aboveSubview: sourceVCView!)
// the animation: first remove the source out of screen by moving it at the left side of it and at the same time place the destination to source's position
// Animate the transition.
UIView.animate(withDuration: 0.3, animations: { () -> Void in
sourceVCView?.transform = CGAffineTransform(translationX: -screenWidth,y: 0)
destinationVCView?.transform = CGAffineTransform.identity
}, completion: { (Finished) -> Void in
self.source.present(self.destination, animated: false, completion: nil)
})
}
}
Could use some help troubleshooting an issue with a SpriteKit scene.
I have a scene that displays some coins in the main section of the app.
When I present a viewcontroller from the bottom I have no issue. Same for tab bar navigation, no issues.
Here is the view as it should always be displayed.
The issue comes only when I present a viewcontroller from the side.
When the new viewcontroller is dismissed, the scene works, but is distorted.
this is how it is displayed after a viewcontroller is displayed modally and later on dismissed.
EDIT: I forgot to mention that if I swipe vertically on the distorted scene, the distortion is fixed and all is good.
Here is some of the code in viewDidAppear of the viewcontroller.
Thanks for the help.
EDIT 2:
I just tested the app on a iPhone 5 using iOS 10 and the issue doesn't happen. Any chance this might be iOS 11 related?
func configureScene(_ completion: () -> Void) {
defer { completion() }
guard wScene == nil else { return }
let skView = SKView(frame: self.view.frame)
skView.isUserInteractionEnabled = false
skView.backgroundColor = .clear
wScene = WScene(size: view.frame.size)
wScene.backgroundColor = .clear
skView.presentScene(wScene)
view.insertSubview(skView, belowSubview: collectionView)
if let buttonsObstacle = doubleButton?.buttonsView {
let obstacleSize = CGSize.init(width: buttonsObstacle.frame.width, height: buttonsObstacle.frame.height)
obstacle = SKSpriteNode.init(color: .clear, size: obstacleSize)
guard let obstacle = obstacle else { return }
obstacle.name = WScene.obstacleNodeName
let convertedOrigin = view.convert(buttonsObstacle.center, from: buttonsObstacle.superview)
let skConvertedOrigin = skView.convert(convertedOrigin, to: wScene)
obstacle.position = skConvertedOrigin
obstacle.physicsBody = SKPhysicsBody(rectangleOf: obstacleSize)
obstacle.physicsBody?.allowsRotation = false
obstacle.physicsBody?.isDynamic = false
source.scrollHandler = { [weak self] (scrollView) in
guard let strongSelf = self else { return }
strongSelf.buttonsMoved(inView: skView, withScroll: scrollView)
}
wScene.addChild(obstacle)
presenter.loadData()
}
}
I solved my issue.
It was related to the new iOS 11 adjustedContentInset property.
My Coin SK scene was being moved by the scroll handler when the view appeared after a modal transition.
My solution is to disable the scrolling for the first 0.1 second after the view appears. In this way iOS 11 doesn't touch the coins anymore while users are able to scroll correctly because they interact with the view most of the time after at least 0.1 seconds.