I need to scroll the table to bottom.When i write the code of scrolling the table in viewwilAppear it does not work,but when i write the same code in viewDidAppear it works but user see the scrolling. I do not want to see the user , scrolling how to make tableScrolling in ViewWillAppear.`
override func viewWillAppear(animated: Bool)
{
super.viewWillAppear(animated)
UIApplication.sharedApplication().delegate!.window!!.windowLevel = UIWindowLevelNormal
self.view.backgroundColor = UIColor.whiteColor()
viewWillAppearExecuted = true
executeCodeOfViewViewAppear()
}
func executeCodeOfViewViewAppear() {
if viewWillAppearExecuted {
viewWillAppearExecuted = false
if appNeedsAutoResize{
self.noLongerParticipantMessageLabel.font = UIUtils.getFontForApproprieteField(.Footnote).font
self.typingStatusLabel.font = UIUtils.getFontForApproprieteField(.Headline).font
}
withAnimationFlag = true
if !self.composeBar.textView.isFirstResponder()
{
// This block will execute if compose bar is first responder
self.updateTableBottomCnt(value:self.composeBar.frame.size.height)
// Update compose bar constarint
if self.composeBarBottomConstraing.constant != 0
{
self.composeBarBottomConstraing.constant = 0
}
}
self.isOpenNextScreen = false
self.checkIfloginUserisActiveMember()
self.tabBarController?.tabBar.hidden = true
ChatCommonCall.sharedInstance.currentThreadIdForPush = self.threadInfo.threadId
// This block is used for make a responder to keyboard for send message.
if shouldComposeBarFirstResponder == true {
self.composeBar.becomeFirstResponder()
shouldComposeBarFirstResponder = false
}
self.showMessageById(self.messageIdForMessageDisplay)
ChatCommonCall.sharedInstance.isChatModuleScreen = true
self.changeFrame()
self.composeBar.button.enabled = self.composeBar.text.trim().length > 0 ? true : false
/// Table frame changes.
switch self.optType
{
case .NewMessageSection:
break
case .DoNotHandle :
break
case .None :
if !(self.messageIdForMessageDisplay.isEqual(Guid.emptyGuid()))
{
self.optType = .ShowMessageOfID(id: self.messageIdForMessageDisplay)
}
else {
self.optType = .ScrollToBottom
}
break
default :
break
}
switch self.optType
{
case .NewMessageSection :
break
default :
self.handleOptType()
}
//Implemented the new observer pattern in the whole project where we suppose to refresh the screen for background sync data
//By Nikhil Kumar Saraf on 19 July 2016
Notifier.Unregister()
let observer = DatabaseObserverInfo(tName: ["chatthread","chatthreadmember","chatmessage","chatmessagereceiver","chatthumbnail","pfthumbnail","chatmessagemarker"], oType: [SyncOpType.Insert,SyncOpType.Update,SyncOpType.Delete], observer: self, stick: false, syncTrigger: SyncTriggerType.Post)
Notifier.Register(observer)
//By Balkrishan Yadav for AutoResizing UI on 30 Nov. 2016
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ChatMessageController.didChangePreferredContentSize(_:)), name:UIContentSizeCategoryDidChangeNotification, object: nil)
// This condition will execute if some one sharing image and coming to the chat message screen
//#Balkrishan Yadav
if ChatCommonCall.sharedInstance.attachmentInfo != nil {
self.addFileInComposeBar(ChatCommonCall.sharedInstance.attachmentInfo!)
ChatCommonCall.sharedInstance.attachmentInfo = nil
let dispatchTime: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(0.3 * Double(NSEC_PER_SEC)))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
// your function here
// self.changeController(true)
})
}
}
}
func handleOptType(callFromViewDidAppear:Bool = false) {
switch optType {
case .ScrollToBottom : // No new message is available. show last message. self.messageTableView.setContentOffset(CGPointMake(0,CGFloat.max), animated: !isCameFromThreadComposer)
break
default:
// do nothing
}
If you don't want the scrolling to be animated you just have to pass false as the animated argument to the scrolling function. For example :
tableView.scrollToRow(at: indexPath, at: .top, animated: false)
This function would scroll the indexPath's row to the top of the table view without any animation.
Related
I have a radial animation on programmatically created button which appear when the button is pressed.
If the button is pressed whilst the animation is already running, I want the animation to reset at the beginning and automatically play again.
In the "animationDidStop" function, I have code which applies when the animation does complete. The issue I have is when the button is pressed and an animation is already running. I can't remove the animation running and start a new one as it calls the "animationDidStop". I don't want this to be called yet.
My ideas was to adjust the animation value attached to the radial shape but this isn't working has planned . I know can access the animation values but which one would I change as "fromValue" doesn't show up. I also don't even know if this would actually work though or a better way. I been stuck for a while now.
var shapeLayerArray: [CustomShapeLayer] = []
func AutoUpRadial(button: CustomBtn, height: Int, value: Int){
let trackLayer = CustomShapeLayer()
let radius = height / 3
let circularPath = UIBezierPath(arcCenter: button.center, radius: CGFloat(radius), startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.black.cgColor
trackLayer.opacity = 0.3
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineWidth = 5
trackLayer.strokeEnd = 0
mainScrollView.layer.addSublayer(trackLayer)
autoUpFillRadial(value: value, tmpBtn: button, shape: trackLayer)
shapeLayerArray.append(trackLayer)
}
#objc private func autoUpFillRadial(value: Int, tmpBtn: CustomBtn, shape: CustomShapeLayer){
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.fromValue = 0
basicAnimation.toValue = 1
basicAnimation.duration = CFTimeInterval(value)
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = true
basicAnimation.setValue(tmpBtn.UUIDtag, forKey: "animationID")
basicAnimation.delegate = self
shape.add(basicAnimation, forKey: "basic")
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if let tag = anim.value(forKey: "animationID") as? UUID {
if let tmpButton: CustomBtn = tmpButtonPicked(uuid: tag) {
tmpButton.isSelected = false
actionInput(sender: tmpButton, act: "up")
}
}
}
Below is the button script
#IBAction func clipButton(sender: CustomBtn){
currentSelectedMarkerUUID = sender.UUIDtag
if let marker = markUpPlist.arrayObjects.filter({$0.UUIDpic == currentSelectedMarkerUUID}).first {
if recording {
if sender.isSelected {
if !shapeLayerArray.isEmpty {
for shape in shapeLayerArray {
if shape.uuidTag == marker.UUIDpic {
print(shape.animation(forKey: "basic"))
// The line aboves shows the animation data
sender.isSelected = true
} else {
sender.isSelected = false
endClip(sender: sender)
}
}
} else if shapeLayerArray.isEmpty {
sender.isSelected = false
endClip(sender: sender)
}
} else if !sender.isSelected {
sender.isSelected = true
startClip(sender: sender)
if marker.timeAutoUp > 0 {
if !shapeLayerArray.isEmpty {
for shape in shapeLayerArray {
if shape.uuidTag == marker.UUIDpic {
//DO SOMETHING
} else {
self.AutoUpRadial(button: sender, height: marker.height, value: marker.timeAutoUp)
}
}
} else if shapeLayerArray.isEmpty {
self.AutoUpRadial(button: sender, height: marker.height, value: marker.timeAutoUp)
}
}
} else {
sender.isSelected = false
}
}
}
}
class CustomShapeLayer: CAShapeLayer {
var uuidTag: UUID?
}
So to recap, I want to be able to reset the animation if the animation is already playing when the button is pressed but without calling the animationDidStop function.
I tried to remove as much stuff irrelevant to the code to condense it as much as I could. Hopefully it makes sense. Thanks for any advice
After hours stuck on it I came up with this. I added a reset boolean in my custom CAshapelayer. The reset bool gets called when the button is pressed and the animation is already running.
class CustomShapeLayer: CAShapeLayer {
var uuidTag: UUID?
vart reset = false
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if let tag = anim.value(forKey: "animationID") as? UUID {
if let tmpButton: CustomBtn = tmpButtonPicked(uuid: tag) {
if !shapeLayerArray.isEmpty {
for shape in shapeLayerArray {
if shape.uuidTag == tag{
if shape.reset {
shape.reset = false
} else {
tmpButton.isSelected = false
actionInput(sender: tmpButton, act: "up")
}
} else {
tmpButton.isSelected = false
actionInput(sender: tmpButton, act: "up")
}
}
} else if shapeLayerArray.isEmpty {
tmpButton.isSelected = false
actionInput(sender: tmpButton, act: "up")
}
}
}
}
I have some troubles to get the views transitions animated when I change the UITabBarController.selectedIndex changed programmatically.
When I tap on a TabBar icon the animation works fine, but when I change the selectedIndex from a gestureRecognizer action.
The transition code at the TabBar controller class is the following:
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if CanChangeTab {
guard let fromView = tabBarController.selectedViewController!.view, let toView = viewController.view else {
return false // Make sure you want this as false
}
if fromView != toView {
if (tabBarController.prevIndex > tabBarController.selectedIndex) {
UIView.transition(from: fromView, to: toView, duration: 0.3, options: [.transitionFlipFromLeft], completion: nil)
} else {
UIView.transition(from: fromView, to: toView, duration: 0.3, options: [.transitionFlipFromRight], completion: nil)
}
}
return true
} else {
return false
}
}
The gesture recogniser is calling the following function, from which the above code is not called:
#objc func swiped(_ gesture: UISwipeGestureRecognizer) {
if (CanChangeTab) {
self.tabBarController?.prevIndex = (self.tabBarController?.selectedIndex)!
if gesture.direction == .left {
if (self.tabBarController?.selectedIndex)! < 4 { // set your total tabs here
self.tabBarController?.selectedIndex += 1
}
} else if gesture.direction == .right {
if (self.tabBarController?.selectedIndex)! > 0 {
self.tabBarController?.selectedIndex -= 1
}
}
}
}
I cannot see what should be called or overridden to get the animations for the gesture base change too.
The problem is that what you are doing is not how to do a tab bar controller animation. You have to write a formally structured custom animation transition.
This means that:
Your tab bar controller has a delegate implementing animationControllerForTransitionFrom to return a UIViewControllerAnimatedTransitioning object, and interactionControllerFor to return a UIViewControllerInteractiveTransitioning object.
These objects implement startInteractiveTransition, interruptibleAnimator(using:), transitionDuration(using:), animateTransition(using:), and animationEnded to perform the animation through a UIViewPropertyAnimator.
The gesture recognizer will then be able to trigger the animation by setting the selectedIndex and will be able to track and update the animation through the supplied UIViewControllerContextTransitioning object and the UIViewPropertyAnimator.
Ok. I have found the solution, using ViewController slide animation
as Matt proposed.
So using the extension + the animateToTab function in the extension and changing my swipe method it works just as expected.
#objc func swiped(_ gesture: UISwipeGestureRecognizer) {
if (CanChangeTab) {
let thisTabController = self.tabBarController as! iBayTabController
thisTabController.prevIndex = (thisTabController.selectedIndex)
if gesture.direction == .left {
if thisTabController.selectedIndex < 4 { // set your total tabs here
thisTabController.animateToTab(toIndex: thisTabController.selectedIndex+1)
}
} else if gesture.direction == .right {
if (self.tabBarController?.selectedIndex)! > 0 {
thisTabController.animateToTab(toIndex: thisTabController.selectedIndex-1)
}
}
}
}
In interface builder, I have several views (A, B, C) in a NSStackView (vertical orientation).
During runtime, I change dynamically the NSStackView by showing or hiding (isHidden) some of these embedded views through a property observer (willSet). If the code below actually works (the views show or hide accordingly), I can't manage to do it with animation.
var isExpanded :Bool = false {
willSet {
NSAnimationContext.beginGrouping()
NSAnimationContext.current.duration = 2.0
if newValue {
viewA.isHidden = true
viewB.isHidden = false
viewC.isHidden = true
viewD.isHidden = true
print("Popover expanded")
} else {
viewA.isHidden = false
viewB.isHidden = false
viewC.isHidden = false
viewD.isHidden = false
print("Popover contracted")
}
NSAnimationContext.endGrouping()
}
As I understand, the isHidden state is not handled by the animation but I don't find other ways to do it.
Alternatively, I also tried to use addView and removeFromSuperview method (instead of hiding/showing). Same results...
My problem is that I mainly find iOS-related problems (UIView.animate...), and none about MacOS (NSView)...
Any ideas ?
Many thanks for your help, Jo
I had the wrong approach: isHidden is not the right approach (can't animate a discrete value - it's hidden or not).
Instead, I added a constraint on the view's height
Connect the constraint in the viewController as an IBOutlet. With this code, the view smoothly squeeze in between 2 other views in a stackView. :-)
#IBOutlet weak var constraint: NSLayoutConstraint!
#IBAction func toggle(_ sender: NSButton) {
if constraint.constant == 0 {
NSAnimationContext.runAnimationGroup({context in
context.duration = 0.25
context.allowsImplicitAnimation = true
constraint.constant = 80
self.view.layoutSubtreeIfNeeded()
}, completionHandler: nil)
} else {
NSAnimationContext.runAnimationGroup({context in
context.duration = 0.25
context.allowsImplicitAnimation = true
constraint.constant = 0
self.view.layoutSubtreeIfNeeded()
}, completionHandler: nil)
}
}
Hope it helps.
Jo
I'm trying to implement adding new portion of information at the top, when user is scrolling to the top (to see chat history) without any jumping like in Telegram, Whatsup and so on
Here is my method
private var toItem: NSIndexPath?
private func addMoreCells () {
if collectionView.contentOffset.y <= 0 {
guard let userChatUid = user?.chatUid else { return }
guard let ownChatUid = UsersManager.sharedInstance.currentChatUID() else { return }
guard let offset = conversation?.messages.count else { return }
ConversationsManager.sharedInstance.getConversationsWithMoreMessagesFor(ownChatUid, recipient: userChatUid, offset: offset - 1, successHandler: { [weak weakSelf = self] (conversations) in
dispatch_async(dispatch_get_main_queue()) {
if let messagesFromServer = conversations.first?.messages, var messages = weakSelf?.conversation?.messages {
for message in messagesFromServer {
if !messages.contains(message) {
messages.append(message)
}
}
messages = messages.sort({$0.time?.compare($1.time!) == .OrderedAscending})
weakSelf?.conversation?.messages = messages
weakSelf?.collectionView.reloadData()
weakSelf?.toItem = NSIndexPath(forItem: (messages.count - messagesFromServer.count), inSection: 0)
if let item = weakSelf?.toItem{
weakSelf?.collectionView.scrollToItemAtIndexPath(item, atScrollPosition: .Top, animated: false)
}
}
}
}, failHandler: { (error) in
print(error)
})
}
}
which is launched from
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
addMoreCells()
}
First time when I'm scrolling to the top It works as I expect (new portion is added, no jumping. I can see new data and can continue scrolling to the top), but when I continue scrolling to the top and new data are added, it always returns to the position, when first info was added.
But I expect It should return to value messages.count - messagesFromServer.count. In xCode I see numbers are changed every time when I'am scrolling, but scrollToItemAtIndexPath doesn't work properly
And Every time when I scroll to the top and new data are added, It always returns to first position when first info was added.
Can Anyone know what's problem? Cause I cant find solutions to fix it.
I've solved this problem like this:
After reloadData() I save contentSize of collection view, after that I refresh layout, get new contentSize and subtract them to get needed offset to scroll to this place without jumping.
weakSelf?.collectionView.reloadData()
guard let previousContentSize = weakSelf?.collectionView.contentSize else { return }
weakSelf?.collectionView.collectionViewLayout.invalidateLayout()
weakSelf?.collectionView.collectionViewLayout.prepareLayout()
weakSelf?.collectionView.layoutIfNeeded()
guard let newContentSize = weakSelf?.collectionView.contentSize else { return }
print("previous Content Size \(previousContentSize) and new Content Size \(newContentSize)")
let contentOffset = newContentSize.height - previousContentSize.height
weakSelf?.collectionView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false)
I works great and as I would expect.
is it possible to determine whether my UIView is visible to the user or not?
My View is added as subview several times into a Tab Bar Controller.
Each instance of this view has a NSTimer that updates the view.
However I don't want to update a view which is not visible to the user.
Is this possible?
Thanks
For anyone else that ends up here:
To determine if a UIView is onscreen somewhere, rather than checking superview != nil, it is better to check if window != nil. In the former case, it is possible that the view has a superview but that the superview is not on screen:
if (view.window != nil) {
// do stuff
}
Of course you should also check if it is hidden or if it has an alpha > 0.
Regarding not wanting your NSTimer running while the view is not visible, you should hide these views manually if possible and have the timer stop when the view is hidden. However, I'm not at all sure of what you're doing.
You can check if:
it is hidden, by checking view.hidden
it is in the view hierarchy, by checking view.superview != nil
you can check the bounds of a view to see if it is on screen
The only other thing I can think of is if your view is buried behind others and can't be seen for that reason. You may have to go through all the views that come after to see if they obscure your view.
This will determine if a view's frame is within the bounds of all of its superviews (up to the root view). One practical use case is determining if a child view is (at least partially) visible within a scrollview.
Swift 5.x:
func isVisible(view: UIView) -> Bool {
func isVisible(view: UIView, inView: UIView?) -> Bool {
guard let inView = inView else { return true }
let viewFrame = inView.convert(view.bounds, from: view)
if viewFrame.intersects(inView.bounds) {
return isVisible(view: view, inView: inView.superview)
}
return false
}
return isVisible(view: view, inView: view.superview)
}
Older swift versions
func isVisible(view: UIView) -> Bool {
func isVisible(view: UIView, inView: UIView?) -> Bool {
guard let inView = inView else { return true }
let viewFrame = inView.convertRect(view.bounds, fromView: view)
if CGRectIntersectsRect(viewFrame, inView.bounds) {
return isVisible(view, inView: inView.superview)
}
return false
}
return isVisible(view, inView: view.superview)
}
Potential improvements:
Respect alpha and hidden.
Respect clipsToBounds, as a view may exceed the bounds of its superview if false.
The solution that worked for me was to first check if the view has a window, then to iterate over superviews and check if:
the view is not hidden.
the view is within its superviews bounds.
Seems to work well so far.
Swift 3.0
public func isVisible(view: UIView) -> Bool {
if view.window == nil {
return false
}
var currentView: UIView = view
while let superview = currentView.superview {
if (superview.bounds).intersects(currentView.frame) == false {
return false;
}
if currentView.isHidden {
return false
}
currentView = superview
}
return true
}
I benchmarked both #Audrey M. and #John Gibb their solutions.
And #Audrey M. his way performed better (times 10).
So I used that one to make it observable.
I made a RxSwift Observable, to get notified when the UIView became visible.
This could be useful if you want to trigger a banner 'view' event
import Foundation
import UIKit
import RxSwift
extension UIView {
var isVisibleToUser: Bool {
if isHidden || alpha == 0 || superview == nil {
return false
}
guard let rootViewController = UIApplication.shared.keyWindow?.rootViewController else {
return false
}
let viewFrame = convert(bounds, to: rootViewController.view)
let topSafeArea: CGFloat
let bottomSafeArea: CGFloat
if #available(iOS 11.0, *) {
topSafeArea = rootViewController.view.safeAreaInsets.top
bottomSafeArea = rootViewController.view.safeAreaInsets.bottom
} else {
topSafeArea = rootViewController.topLayoutGuide.length
bottomSafeArea = rootViewController.bottomLayoutGuide.length
}
return viewFrame.minX >= 0 &&
viewFrame.maxX <= rootViewController.view.bounds.width &&
viewFrame.minY >= topSafeArea &&
viewFrame.maxY <= rootViewController.view.bounds.height - bottomSafeArea
}
}
extension Reactive where Base: UIView {
var isVisibleToUser: Observable<Bool> {
// Every second this will check `isVisibleToUser`
return Observable<Int>.interval(.milliseconds(1000),
scheduler: MainScheduler.instance)
.map { [base] _ in
return base.isVisibleToUser
}.distinctUntilChanged()
}
}
Use it as like this:
import RxSwift
import UIKit
import Foundation
private let disposeBag = DisposeBag()
private func _checkBannerVisibility() {
bannerView.rx.isVisibleToUser
.filter { $0 }
.take(1) // Only trigger it once
.subscribe(onNext: { [weak self] _ in
// ... Do something
}).disposed(by: disposeBag)
}
Tested solution.
func isVisible(_ view: UIView) -> Bool {
if view.isHidden || view.superview == nil {
return false
}
if let rootViewController = UIApplication.shared.keyWindow?.rootViewController,
let rootView = rootViewController.view {
let viewFrame = view.convert(view.bounds, to: rootView)
let topSafeArea: CGFloat
let bottomSafeArea: CGFloat
if #available(iOS 11.0, *) {
topSafeArea = rootView.safeAreaInsets.top
bottomSafeArea = rootView.safeAreaInsets.bottom
} else {
topSafeArea = rootViewController.topLayoutGuide.length
bottomSafeArea = rootViewController.bottomLayoutGuide.length
}
return viewFrame.minX >= 0 &&
viewFrame.maxX <= rootView.bounds.width &&
viewFrame.minY >= topSafeArea &&
viewFrame.maxY <= rootView.bounds.height - bottomSafeArea
}
return false
}
I you truly want to know if a view is visible to the user you would have to take into account the following:
Is the view's window not nil and equal to the top most window
Is the view, and all of its superviews alpha >= 0.01 (threshold value also used by UIKit to determine whether it should handle touches) and not hidden
Is the z-index (stacking value) of the view higher than other views in the same hierarchy.
Even if the z-index is lower, it can be visible if other views on top have a transparent background color, alpha 0 or are hidden.
Especially the transparent background color of views in front may pose a problem to check programmatically. The only way to be truly sure is to make a programmatic snapshot of the view to check and diff it within its frame with the snapshot of the entire screen. This won't work however for views that are not distinctive enough (e.g. fully white).
For inspiration see the method isViewVisible in the iOS Calabash-server project
The simplest Swift 5 solution I could come up with that worked in my situation (I was looking for a button embedded in my tableViewFooter).
John Gibbs solution also worked but in my cause I did not need all the recursion.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let viewFrame = scrollView.convert(targetView.bounds, from: targetView)
if viewFrame.intersects(scrollView.bounds) {
// targetView is visible
}
else {
// targetView is not visible
}
}
In viewWillAppear set a value "isVisible" to true, in viewWillDisappear set it to false. Best way to know for a UITabBarController subviews, also works for navigation controllers.
Another useful method is didMoveToWindow()
Example: When you push view controller, views of your previous view controller will call this method. Checking self.window != nil inside of didMoveToWindow() helps to know whether your view is appearing or disappearing from the screen.
This can help you figure out if your UIView is the top-most view. Can be helpful:
let visibleBool = view.superview?.subviews.last?.isEqual(view)
//have to check first whether it's nil (bc it's an optional)
//as well as the true/false
if let visibleBool = visibleBool where visibleBool { value
//can be seen on top
} else {
//maybe can be seen but not the topmost view
}
try this:
func isDisplayedInScreen() -> Bool
{
if (self == nil) {
return false
}
let screenRect = UIScreen.main.bounds
//
let rect = self.convert(self.frame, from: nil)
if (rect.isEmpty || rect.isNull) {
return false
}
// 若view 隐藏
if (self.isHidden) {
return false
}
//
if (self.superview == nil) {
return false
}
//
if (rect.size.equalTo(CGSize.zero)) {
return false
}
//
let intersectionRect = rect.intersection(screenRect)
if (intersectionRect.isEmpty || intersectionRect.isNull) {
return false
}
return true
}
In case you are using hidden property of view then :
view.hidden (objective C) or view.isHidden(swift) is read/write property. So you can easily read or write
For swift 3.0
if(view.isHidden){
print("Hidden")
}else{
print("visible")
}