iOS - Find top constraint for a view? - ios

I am trying to find the top constraint of the view in code.
The top constraint is added in storyboard, and I don't want to use an IBOutlet.
Logging the value of the firstAttribute in the following code seems to always return a constraint of type NSLayoutAttributeHeight. Any idea how I could reliably find a top constraint of a view in code?
NSLayoutConstraint *topConstraint;
for (NSLayoutConstraint *constraint in self.constraints) {
if (constraint.firstAttribute == NSLayoutAttributeTop) {
topConstraint = constraint;
break;
}
}

Instead of iterating through self.constraints, you should iterate through self.superview.constraints.
The self.constraints only contain constraints related to just the view (e.g. height and width constraints).
Here's a code example of what this might look like:
- (void)awakeFromNib
{
[super awakeFromNib];
if (!self.topConstraint) {
[self findTopConstraint];
}
}
- (void)findTopConstraint
{
for (NSLayoutConstraint *constraint in self.superview.constraints) {
if ([self isTopConstraint:constraint]) {
self.topConstraint = constraint;
break;
}
}
}
- (BOOL)isTopConstraint:(NSLayoutConstraint *)constraint
{
return [self firstItemMatchesTopConstraint:constraint] ||
[self secondItemMatchesTopConstraint:constraint];
}
- (BOOL)firstItemMatchesTopConstraint:(NSLayoutConstraint *)constraint
{
return constraint.firstItem == self && constraint.firstAttribute == NSLayoutAttributeTop;
}
- (BOOL)secondItemMatchesTopConstraint:(NSLayoutConstraint *)constraint
{
return constraint.secondItem == self && constraint.secondAttribute == NSLayoutAttributeTop;
}

I usually set an identifier of a required constraint in the IB and then find it in the code like this (Swift):
if let index = constraints.index(where: { $0.identifier == "checkmarkLeftMargin" }) {
checkmarkImageViewLeftMargin = constraints[index]
}
OR by #Tim Vermeulen
checkmarkImageViewLeftMargin = constraints.first { $0.identifier == "checkmarkLeftMargin" }

Using swift and UIView extension
extension UIView {
func findConstraint(layoutAttribute: NSLayoutAttribute) -> NSLayoutConstraint? {
if let constraints = superview?.constraints {
for constraint in constraints where itemMatch(constraint: constraint, layoutAttribute: layoutAttribute) {
return constraint
}
}
return nil
}
func itemMatch(constraint: NSLayoutConstraint, layoutAttribute: NSLayoutAttribute) -> Bool {
if let firstItem = constraint.firstItem as? UIView, let secondItem = constraint.secondItem as? UIView {
let firstItemMatch = firstItem == self && constraint.firstAttribute == layoutAttribute
let secondItemMatch = secondItem == self && constraint.secondAttribute == layoutAttribute
return firstItemMatch || secondItemMatch
}
return false
}
}

Based on #Igor answer, I changed a bit itemMatch method to consider when first item or second item is not a UIView. For example when constraint a UIView top to safe area top.
extension UIView {
func findConstraint(layoutAttribute: NSLayoutConstraint.Attribute) -> NSLayoutConstraint? {
if let constraints = superview?.constraints {
for constraint in constraints where itemMatch(constraint: constraint, layoutAttribute: layoutAttribute) {
return constraint
}
}
return nil
}
func itemMatch(constraint: NSLayoutConstraint, layoutAttribute: NSLayoutConstraint.Attribute) -> Bool {
let firstItemMatch = constraint.firstItem as? UIView == self && constraint.firstAttribute == layoutAttribute
let secondItemMatch = constraint.secondItem as? UIView == self && constraint.secondAttribute == layoutAttribute
return firstItemMatch || secondItemMatch
}
}

Set the identifier in the inspector in Xcode. That's what it's for. You name it.
If that's not enough you create the IBOutlet.

I write a small extension in Swift:
extension UIButton {
var topConstraints: [NSLayoutConstraint]? {
return self.constraints.filter( { ($0.firstItem as? UIButton == self && $0.firstAttribute == .top) || ($0.secondItem as? UIButton == self && $0.secondAttribute == .top) })
}
}

Here's a one-liner extension method, based on #Igor's approach:
extension UIView{
func constraint(for layoutAttribute: NSLayoutConstraint.Attribute) -> NSLayoutConstraint? {
return superview?.constraints.first { itemMatch(constraint: $0, layoutAttribute: layoutAttribute) }
}
private func itemMatch(constraint: NSLayoutConstraint, layoutAttribute: NSLayoutConstraint.Attribute) -> Bool {
if let firstItem = constraint.firstItem as? UIView, let secondItem = constraint.secondItem as? UIView {
let firstItemMatch = firstItem == self && constraint.firstAttribute == layoutAttribute
let secondItemMatch = secondItem == self && constraint.secondAttribute == layoutAttribute
return firstItemMatch || secondItemMatch
}
return false
}
}
[ I also made the method signature style to match Swift 3...5 style e.g.:
constraint(for:) instead of findConstraint(layoutAttribute:).
]

Related

Issue in `becomeFirstResponder()` in swift

I have six textfields. Now if all my textfield are filled and tap on any textfield the it should always put focus on sixth textfield & show the keyboard. I have tried below code but it does not show keyboard and only put focus when I tap on sixth textfield. please tell me what is the issue with this ?
func textFieldDidBeginEditing(_ textField: UITextField) {
textField.inputAccessoryView = emptyView
if let textOneLength = textFieldOne.text?.length ,let textTwoLength = textFieldTwo.text?.length ,let textThreeLength = textFieldThree.text?.length , let textFourLength = textFieldFour.text?.length,let textFiveLength = textFieldFive.text?.length , let textSixLength = textFieldSix.text?.length {
if (textOneLength > 0) && (textTwoLength > 0) && (textThreeLength > 0) && (textFourLength > 0) && (textFiveLength > 0) && (textSixLength > 0) {
self.textFieldSix.becomeFirstResponder()
} else if (textOneLength <= 0) && (textTwoLength <= 0) && (textThreeLength <= 0) && (textFourLength <= 0) && (textFiveLength <= 0) && (textSixLength <= 0){
self.textFieldOne.becomeFirstResponder()
}
}
}
I think accepted answer is hack. Other thing that we can do is detect touchDown on UITextField, check if last textField should be in focus and do becomeFirstResponder() on it. Next thing, we should disallow focus other textFields if last should be in focus. We can do that in textFieldShouldBeginEditing method.
Here example of ViewController. Just connect 3 textFields and all should work as expected (Swift 4):
class ViewController: UIViewController {
#IBOutlet weak var firstTextField: UITextField!
#IBOutlet weak var secondTextField: UITextField!
#IBOutlet weak var thirdTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
firstTextField.delegate = self
secondTextField.delegate = self
thirdTextField.delegate = self
firstTextField.addTarget(self, action: #selector(textFieldTouch(_:)), for: .touchDown)
secondTextField.addTarget(self, action: #selector(textFieldTouch(_:)), for: .touchDown)
thirdTextField.addTarget(self, action: #selector(textFieldTouch(_:)), for: .touchDown)
}
#IBAction private func textFieldTouch(_ sender: UITextField) {
if shouldFocusOnLastTextField {
thirdTextField.becomeFirstResponder()
}
}
private var shouldFocusOnLastTextField: Bool {
return firstTextField.text?.isEmpty == false && secondTextField.text?.isEmpty == false && thirdTextField.text?.isEmpty == false
}
}
extension ViewController: UITextFieldDelegate {
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
guard shouldFocusOnLastTextField else { return true }
return textField == thirdTextField
}
}
Other, more simple way to achieve that check the textField that is going to be focused:
extension ViewController: UITextFieldDelegate {
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
guard firstTextField.text?.isEmpty == false &&
secondTextField.text?.isEmpty == false &&
thirdTextField.text?.isEmpty == false &&
textField != thirdTextField else { return true }
thirdTextField.becomeFirstResponder()
return false
}
}
The problem with your implementation is that you are trying to assign the first responder when it was already assign to another textField.
The following code should do the trick:
extension TestViewController: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
textField.inputAccessoryView = UIView()
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
if let textOneLength = textFieldOne.text?.count ,let textTwoLength = textFieldTwo.text?.count ,let textThreeLength = textFieldThree.text?.count , let textFourLength = textFieldFour.text?.count,let textFiveLength = textFieldFive.text?.count , let textSixLength = textFieldSix.text?.count {
if (textOneLength > 0) && (textTwoLength > 0) && (textThreeLength > 0) && (textFourLength > 0) && (textFiveLength > 0) && (textSixLength > 0) {
///Check if the sixth textField was selected to avoid infinite recursion
if textFieldSix != textField {
self.textFieldSix.becomeFirstResponder()
return false
}
} else if (textOneLength <= 0) && (textTwoLength <= 0) && (textThreeLength <= 0) && (textFourLength <= 0) && (textFiveLength <= 0) && (textSixLength <= 0){
///Check if the first textField was selected to avoid infinite recursion
if textFieldOne != textField {
self.textFieldOne.becomeFirstResponder()
return false
}
}
}
return true
}
}
It should be work. Are you sure that the simulator has the property to show the keywoard enable?
You can change that property with the keys: Command + Shift + k
I know it is strange for me as well but delaying the focus on text field worked for me.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.textFieldOne.becomeFirstResponder()
}
I added above code in textFieldDidBeginEdting & it worked like magic.
In this case, you'll want to reject attempted edits before they happened; when we wish to reject an attempted edit we will also need to set the sixth textfield as the new first responder.
extension MyViewController: UITextFieldDelegate {
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
if textFieldOne.text?.isEmpty == false && textFieldTwo.text?.isEmpty == false && textFieldThree.text?.isEmpty == false && textFieldFour.text?.isEmpty == false && textFieldFive.text?.isEmpty == false && textFieldSix.text?.isEmpty == false {
if textField != textFieldSix {
textFieldSix.becomeFirstResponder()
return false
} else {
return true
}
} else {
return true
}
}
}
1 Set UITextfielddelegate in viewcontroller
2 Set self delegate to every textfield
3 add tag value for every textfield
4 add textfield.becomeFirstResponder()
5 check if condition using tag for take specify textfield value
Reference to this answer https://stackoverflow.com/a/27098244/4990506
A responder object only becomes the first responder if the current
responder can resign first-responder status (canResignFirstResponder)
and the new responder can become first responder.
You may call this method to make a responder object such as a view the first responder. However, you should only call it on that view if
it is part of a view hierarchy. If the view’s window property holds a
UIWindow object, it has been installed in a view hierarchy; if it
returns nil, the view is detached from any hierarchy.
First set delegate to all field
self.textFieldOne.delegate = self
self.textFieldTwo.delegate = self
self.textFieldThree.delegate = self
self.textFieldFour.delegate = self
self.textFieldFive.delegate = self
self.textFieldSix.delegate = self
And then delegate
extension ViewController : UITextFieldDelegate{
func textFieldDidBeginEditing(_ textField: UITextField) {
if self.textFieldOne.hasText ,self.textFieldTwo.hasText ,self.textFieldThree.hasText ,self.textFieldFour.hasText ,self.textFieldFive.hasText ,self.textFieldSix.hasText {
textFieldSix.perform(
#selector(becomeFirstResponder),
with: nil,
afterDelay: 0.1
)
}else if !self.textFieldOne.hasText ,!self.textFieldTwo.hasText ,!self.textFieldThree.hasText ,!self.textFieldFour.hasText ,!self.textFieldFive.hasText ,!self.textFieldSix.hasText {
textFieldOne.perform(
#selector(becomeFirstResponder),
with: nil,
afterDelay: 0.1
)
}
}
}
To check any textfield use this code
for view in self.view.subviews {
if let textField = view as? UITextField {
print(textField.text!)
}
}
** you can also identify a textfiled by using this -
if let txtFieldINeed = self.view.viewWithTag(99) as? UITextField {
print(txtFieldINeed.text!)
}
** then use --
txtFieldINeed.becomeFirstResponder()
** Make sure to turn on this options --
* iOS Simulator -> Hardware -> Keyboard
* Uncheck "Connect Hardware Keyboard"

Swift UI test, how to check if a cell has an imageview

In UI test, I can get the first cell using this code:
let app = XCUIApplication()
app.launch()
let tablesQuery = app.tables
let cell = tablesQuery.children(matching:.any).element(boundBy: 0)
How to check if that cell contains a imageview ?
public func hasImageViewInside(_ cell: UITableViewCell) -> Bool {
for child in cell.subviews {
if let _ = child as? UIImageView {
return true
}
}
return false
}
for viw in cell.contentView.subviews {
if ((viw as? UIImageView) != nil) {
print("123")
}
}
Swift 5
Give the cell's ImageView an accessibility identifier first either in storyboard or ViewDidLoad
func testIsImageViewNil() {
let imageView = app.images["PhotosCollectionViewController.ImageCell.ImageView"]
XCTAssertNotNil(imageView)
}
for case let imageView as UIImageView in cell.contentView.subviews {
if imageView.tag == 1001 {
imageView.image = UIImage(named: "myCustomImage")
}
}
//OR Altervnatively
cell.contentView.subviews.flatMap { $0 as? UIImageView }.forEach { imageView in
if imageView.tag == 1001 {
imageView.image = UIImage(named: "myCustomImage")
}
}

Control "active" dot in UIPageControl

My designer is asking that I display 3 dots in a UIPageViewController for 10 views.
When the first 3 view controllers display, the 0th dot should be highlighted; when the next 4 view controllers display, the 1st dot should be highlighted; when the final 3 view controllers display, the 2nd dot should be highlighted.
So far I'm able to display 3 dots in the UIPageControl, but the indicator dot just rotates around indicating the n%3 position as active.
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
return 3
}
I haven't seen any documentation on how to control with UIPageControl index is the active index, so I'm not sure if this is something Apple wants you to be able to override.
If there is a way to accomplish what I'm trying to do, I'd appreciate the help.
It turns out that what I'm trying to accomplish can't be done with a UIPageViewController. By default the UIPageControl in this class cannot be overridden directly.
Instead, I was able to use a combination of a UICollectionView (with a hack that allows it to resemble a UIPageViewController in its page changing effects) and a UIPageControl, as subviews to the same overarching UIViewController.
class MyPageViewController : UIViewController {
// MARK: subviews
private var collectionView:UICollectionView!
/// the collection layout controls the scrolling behavior of the collection view
private var collectionLayout = MyLayout()
private var pageControl = UIPageControl()
let CollectionViewCellReuseIdentifer = "CollectionViewCellReuseIdentifier"
// MARK: autolayout
private var autolayoutConstraints:[NSLayoutConstraint] = [NSLayoutConstraint]()
// MARK: constructors
init() {
super.init(nibName: nil, bundle: nil)
}
// MARK: UIViewController lifecycle methods
override func viewDidLoad() {
super.viewDidLoad()
self.setupView()
}
/**
Set up the collection view, page control, skip & log in buttons
*/
func setupView() {
self.setupCollectionView()
self.setupPageControl()
self.setupConstraints()
self.view.addConstraints(self.autolayoutConstraints)
}
/**
Set up the collection view
*/
func setupCollectionView() {
self.collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.collectionLayout)
self.collectionView.translatesAutoresizingMaskIntoConstraints = false
self.collectionView.registerClass(MyPageView.self, forCellWithReuseIdentifier: self.CollectionViewCellReuseIdentifer)
self.collectionView.dataSource = self
self.collectionView.delegate = self
self.collectionView.backgroundColor = UIColor.whiteColor()
self.collectionView.scrollEnabled = true
self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast;
self.collectionLayout.minimumInteritemSpacing = 1
self.collectionLayout.minimumLineSpacing = 1
self.collectionLayout.scrollDirection = .Horizontal
self.collectionLayout.delegate = self
self.view.addSubview(self.collectionView)
}
/**
Set up view showing pagination dots for slideshow items
*/
func setupPageControl() {
self.pageControl.translatesAutoresizingMaskIntoConstraints = false
self.pageControl.numberOfPages = 3
self.pageControl.backgroundColor = UIColor.whiteColor()
self.view.addSubview(self.pageControl)
}
func setupConstraints() {
let views:[String:AnyObject] = [
"collectionView" : self.collectionView,
"pageControl" : self.pageControl,
]
self.autolayoutConstraints.appendContentsOf(
NSLayoutConstraint.constraintsWithVisualFormat(
"V:|[collectionView][pageControl]|",
options: .AlignAllCenterX,
metrics: nil,
views: views
)
)
self.autolayoutConstraints.appendContentsOf(
NSLayoutConstraint.constraintsWithVisualFormat(
"H:|[collectionView]|",
options: .AlignAllCenterY,
metrics: nil,
views: views
)
)
self.autolayoutConstraints.appendContentsOf(
NSLayoutConstraint.constraintsWithVisualFormat(
"H:|[pageControl]|",
options: NSLayoutFormatOptions(),
metrics: nil,
views: views
)
)
}
}
extension MyPageViewController : MyPageViewControllerDelegate {
func didSwitchToPage(imageIndex: Int) {
if imageIndex < 3 {
self.pageControl.currentPage = 0
} else if imageIndex < 7 {
self.pageControl.currentPage = 1
} else {
self.pageControl.currentPage = 2
}
self.pageControl.setNeedsDisplay()
}
}
The layout class was derived from an answer my coworker found when researching a similar issue. http://karmadust.com/centered-paging-with-preview-cells-on-uicollectionview/
/**
* Delegate for slide interactions
*/
protocol MyPageViewControllerDelegate {
/**
Triggered when a new page has been 'snapped' into place
- parameter imageIndex: index of the image that has been snapped to
*/
func didSwitchToPage(imageIndex: Int)
}
class MyLayout : UICollectionViewFlowLayout {
var delegate:MyPageViewControllerDelegate?
/*
Allows different items in the collection to 'snap' onto the current screen section.
Based off of http://karmadust.com/centered-paging-with-preview-cells-on-uicollectionview/
*/
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
if let cv = self.collectionView {
let cvBounds = cv.bounds
let halfWidth = cvBounds.size.width * 0.5;
let proposedContentOffsetCenterX = proposedContentOffset.x + halfWidth;
if let attributesForVisibleCells = self.layoutAttributesForElementsInRect(cvBounds) {
var candidateAttributes : UICollectionViewLayoutAttributes?
// the index of the image selected
var index:Int = 0
for attributes in attributesForVisibleCells {
// == Skip comparison with non-cell items (headers and footers) == //
if attributes.representedElementCategory != UICollectionElementCategory.Cell {
index++
continue
}
if let candAttrs = candidateAttributes {
let a = attributes.center.x - proposedContentOffsetCenterX
let b = candAttrs.center.x - proposedContentOffsetCenterX
if fabsf(Float(a)) < fabsf(Float(b)) {
candidateAttributes = attributes;
}
}
else { // == First time in the loop == //
candidateAttributes = attributes;
index++
continue;
}
}
// Beautification step , I don't know why it works!
if(proposedContentOffset.x == -(cv.contentInset.left)) {
return proposedContentOffset
}
if let delegate = self.delegate {
delegate.didSwitchToPage((candidateAttributes?.indexPath.row)!)
}
return CGPoint(x: floor(candidateAttributes!.center.x - halfWidth), y: proposedContentOffset.y)
}
}
// fallback
return super.targetContentOffsetForProposedContentOffset(proposedContentOffset)
}
}
Note: I trimmed down the actual code I used and replaced a bunch of names to make them more appropriate for examples. I did not run this specific code and did not test for errors in my IDE. That being said, the approach behind the code is solid.

Override layout.subviews deletion control in Swift

I am trying to implement similar code to this in a Swift project
https://gist.github.com/joaofranca/3159618
I am having difficulty getting the class for the subview in the NSStringFromClass sections.
I have tried NSStringFromClass(subview.class) but Swift doesn't like it.
Do you know how to use this in Swift?
Thanks,
Andy
Update:
You can call classForCoder on classes derived from NSObject:
var s: NSObject = "hello"
var i: NSObject = 3
NSStringFromClass(s.classForCoder) // "NSString"
NSStringFromClass(i.classForCoder) // "NSNumber"
Original answer:
In Swift, instead of identifying a class by name, use is:
Objective-C:
for (UIView *subview in self.subviews) {
if([NSStringFromClass([subview class]) isEqualToString:#"UITableViewCellDeleteConfirmationControl"]) {
// do magic here
...
}else if ([NSStringFromClass([subview class]) isEqualToString:#"UITableViewCellEditControl"]) {
// do magic here
...
}else if ([NSStringFromClass([subview class]) isEqualToString:#"UITableViewCellReorderControl"]) {
// do magic here
Swift:
for subview in self.subviews as [UIView] {
if subview is UITableViewCellDeleteConfirmationControl {
// do magic here
...
} else if subview is UITableViewCellEditControl {
// do magic here
...
} else if subview is UITableViewCellReorderControl {
// do magic here
Swift 2.0 ->
Override the layoutSubviews()
class MyCustomCell: UITableViewCell {
override func aSubView() {
super.layoutSubviews()
for aSubView in self.subviews {
if String(aSubView.classForCoder).rangeOfString("UITableViewCellDeleteConfirmationView") != nil {
// Do whatever you want to do with default Delete Button.
// aSubView is the Delete Button.
aSubView.frame = CGRectMake(aSubView.frame.origin.x, aSubView.frame.origin.y, aSubView.frame.size.width, aSubView.frame.size.height - 10)
}
}
}
}
swift 3.0 -> using constraints.
override func layoutSubviews() {
super.layoutSubviews()
for aSubView in self.subviews as [UIView] {
if String(describing: aSubView.classForCoder).range(of: "UITableViewCellDeleteConfirmationView") != nil {
aSubView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
aSubView.widthAnchor.constraint(equalToConstant: 50),
aSubView.heightAnchor.constraint(equalToConstant: 50),
])
}
}
}

Shared ancestor between two views

What is the most efficient way to find the lowest common ancestor between two UIView instances?
Short of implementing Lowest Common Ancestor, are there any UIKit APIs that can be leveraged to find it?
NSView has ancestorSharedWithView: so I suspect this might be added sooner than later to iOS.
I'm currently using this quick and dirty solution, which is inefficient if the given view isn't a sibling or direct ancestor.
- (UIView*)lyt_ancestorSharedWithView:(UIView*)aView
{
if (aView == nil) return nil;
if (self == aView) return self;
if (self == aView.superview) return self;
UIView *ancestor = [self.superview lyt_ancestorSharedWithView:aView];
if (ancestor) return ancestor;
return [self lyt_ancestorSharedWithView:aView.superview];
}
(for those implementing a similar method, the unit tests of the Lyt project might be helpful)
It's not too hard, using -isDescendantOfView:.
- (UIView *)my_ancestorSharedWithView:(UIView *)aView
{
UIView *testView = self;
while (testView && ![aView isDescendantOfView:testView])
{
testView = [testView superview];
}
return testView;
}
Swift 3:
extension UIView {
func findCommonSuperWith(_ view:UIView) -> UIView? {
var a:UIView? = self
var b:UIView? = view
var superSet = Set<UIView>()
while a != nil || b != nil {
if let aSuper = a {
if !superSet.contains(aSuper) { superSet.insert(aSuper) }
else { return aSuper }
}
if let bSuper = b {
if !superSet.contains(bSuper) { superSet.insert(bSuper) }
else { return bSuper }
}
a = a?.superview
b = b?.superview
}
return nil
}
}
A functional alternative:
Swift (assuming the use of your favourite OrderedSet)
extension UIView {
func nearestCommonSuperviewWith(other: UIView) -> UIView {
return self.viewHierarchy().intersect(other.self.viewHierarchy()).first
}
private func viewHierarchy() -> OrderedSet<UIView> {
return Set(UIView.hierarchyFor(self, accumulator: []))
}
static private func hierarchyFor(view: UIView?, accumulator: [UIView]) -> [UIView] {
guard let view = view else {
return accumulator
}
return UIView.hierarchyFor(view.superview, accumulator: accumulator + [view])
}
}
Objective-C (implemented as a category on UIView, assuming the existence of a firstObjectCommonWithArray method)
+ (NSArray *)hierarchyForView:(UIView *)view accumulator:(NSArray *)accumulator
{
if (!view) {
return accumulator;
}
else {
return [self.class hierarchyForView:view.superview accumulator:[accumulator arrayByAddingObject:view]];
}
}
- (NSArray *)viewHierarchy
{
return [self.class hierarchyForView:self accumulator:#[]];
}
- (UIView *)nearestCommonSuperviewWithOtherView:(UIView *)otherView
{
return [[self viewHierarchy] firstObjectCommonWithArray:[otherView viewHierarchy]];
}
Here's a little shorter version, as a category on UIView:
- (UIView *)nr_commonSuperview:(UIView *)otherView
{
NSMutableSet *views = [NSMutableSet set];
UIView *view = self;
do {
if (view != nil) {
if ([views member:view])
return view;
[views addObject:view];
view = view.superview;
}
if (otherView != nil) {
if ([views member:otherView])
return otherView;
[views addObject:otherView];
otherView = otherView.superview;
}
} while (view || otherView);
return nil;
}
Your implementation only check two view level in one iteration.
Here is mine:
+ (UIView *)commonSuperviewWith:(UIView *)view1 anotherView:(UIView *)view2 {
NSParameterAssert(view1);
NSParameterAssert(view2);
if (view1 == view2) return view1.superview;
// They are in diffrent window, so they wont have a common ancestor.
if (view1.window != view2.window) return nil;
// As we don’t know which view has a heigher level in view hierarchy,
// We will add these view and their superview to an array.
NSMutableArray *mergedViewHierarchy = [#[ view1, view2 ] mutableCopy];
UIView *commonSuperview = nil;
// Loop until all superviews are included in this array or find a view’s superview in this array.
NSInteger checkIndex = 0;
UIView *checkingView = nil;
while (checkIndex < mergedViewHierarchy.count && !commonSuperview) {
checkingView = mergedViewHierarchy[checkIndex++];
UIView *superview = checkingView.superview;
if ([mergedViewHierarchy containsObject:superview]) {
commonSuperview = superview;
}
else if (checkingView.superview) {
[mergedViewHierarchy addObject:superview];
}
}
return commonSuperview;
}
Mine is a bit longer and without using UIKit isDescendant function.
Method 1: With a method of finding LCA in trees. Time complexity:O(N), Space complexity: (1)
func findCommonSuper(_ view1:inout UIView, _ view2:inout UIView) -> UIView? {
var level1 = findLevel(view1)
var level2 = findLevel(view2)
if level1 > level2 {
var dif = level1-level2
while dif > 0 {
view1 = view1.superview!
dif -= 1
}
} else if level1 < level2 {
var dif = level2-level1
while dif > 0 {
view2 = view2.superview!
dif -= 1
}
}
while view1 != view2 {
if view1.superview == nil || view2.superview == nil {
return nil
}
view1 = view1.superview!
view2 = view2.superview!
}
if view1 == view2 {
return view1
}
return nil
}
func findLevel(_ view:UIView) -> Int {
var level = 0
var view = view
while view.superview != nil {
view = view.superview!
level += 1
}
return level
}
Method 2: Inserting one view's ancestors to set and then iterating second ones ancestors. Time complexity: O(N), Space complexity: O(N)
func findCommonSuper2(_ view1:UIView, _ view2:UIView) -> UIView? {
var set = Set<UIView>()
var view = view1
while true {
set.insert(view)
if view.superview != nil {
view = view.superview!
} else {
break
}
}
view = view2
while true {
if set.contains(view) {
return view
}
if view.superview != nil {
view = view.superview!
} else {
break
}
}
return nil
}
Swift 2.0:
let view1: UIView!
let view2: UIView!
let sharedSuperView = view1.getSharedSuperview(withOtherView: view2)
/**
* A set of helpful methods to find shared superview for two given views
*
* #author Alexander Volkov
* #version 1.0
*/
extension UIView {
/**
Get nearest shared superview for given and otherView
- parameter otherView: the other view
*/
func getSharedSuperview(withOtherView otherView: UIView) {
(self.getViewHierarchy() as NSArray).firstObjectCommonWithArray(otherView.getViewHierarchy())
}
/**
Get array of views in given view hierarchy
- parameter view: the view whose hierarchy need to get
- parameter accumulator: the array to accumulate views in
- returns: the list of views from given up to the top most view
*/
class func getHierarchyForView(view: UIView?, var accumulator: [UIView]) -> [UIView] {
if let superview = view?.superview {
accumulator.append(view!)
return UIView.getHierarchyForView(superview, accumulator: accumulator)
}
return accumulator
}
/**
Get array of views in the hierarchy of the current view
- returns: the list of views from cuurent up to the top most view
*/
func getViewHierarchy() -> [UIView] {
return UIView.getHierarchyForView(self, accumulator: [])
}
}
Swift 5 version of Carl Lindberg's solution:
func nearestCommonSuperviewWith(other: UIView) -> UIView? {
var nearestAncestor: UIView? = self
while let testView = nearestAncestor, !other.isDescendant(of: testView) {
nearestAncestor = testView.superview
}
return nearestAncestor
}

Resources