NSArray+PureLayout.m 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. //
  2. // NSArray+PureLayout.m
  3. // https://github.com/PureLayout/PureLayout
  4. //
  5. // Copyright (c) 2012 Richard Turton
  6. // Copyright (c) 2013-2015 Tyler Fox
  7. //
  8. // This code is distributed under the terms and conditions of the MIT license.
  9. //
  10. // Permission is hereby granted, free of charge, to any person obtaining a copy
  11. // of this software and associated documentation files (the "Software"), to
  12. // deal in the Software without restriction, including without limitation the
  13. // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  14. // sell copies of the Software, and to permit persons to whom the Software is
  15. // furnished to do so, subject to the following conditions:
  16. //
  17. // The above copyright notice and this permission notice shall be included in
  18. // all copies or substantial portions of the Software.
  19. //
  20. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  21. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  22. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  23. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  24. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  25. // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  26. // IN THE SOFTWARE.
  27. //
  28. #import "NSArray+PureLayout.h"
  29. #import "ALView+PureLayout.h"
  30. #import "NSLayoutConstraint+PureLayout.h"
  31. #import "PureLayout+Internal.h"
  32. #pragma mark - NSArray+PureLayout
  33. @implementation NSArray (PureLayout)
  34. #pragma mark Array of Constraints
  35. /**
  36. Activates the constraints in this array.
  37. */
  38. - (void)autoInstallConstraints
  39. {
  40. #if PL__PureLayout_MinBaseSDK_iOS_8_0 || PL__PureLayout_MinBaseSDK_OSX_10_10
  41. if ([NSLayoutConstraint respondsToSelector:@selector(activateConstraints:)]) {
  42. for (id object in self) {
  43. if ([object isKindOfClass:[NSLayoutConstraint class]]) {
  44. [NSLayoutConstraint al_applyGlobalStateToConstraint:object];
  45. }
  46. }
  47. if ([NSLayoutConstraint al_preventAutomaticConstraintInstallation]) {
  48. [[NSLayoutConstraint al_currentArrayOfCreatedConstraints] addObjectsFromArray:self];
  49. } else {
  50. [NSLayoutConstraint activateConstraints:self];
  51. }
  52. return;
  53. }
  54. #endif /* PL__PureLayout_MinBaseSDK_iOS_8_0 || PL__PureLayout_MinBaseSDK_OSX_10_10 */
  55. for (id object in self) {
  56. if ([object isKindOfClass:[NSLayoutConstraint class]]) {
  57. [((NSLayoutConstraint *)object) autoInstall];
  58. }
  59. }
  60. }
  61. /**
  62. Deactivates the constraints in this array.
  63. */
  64. - (void)autoRemoveConstraints
  65. {
  66. #if PL__PureLayout_MinBaseSDK_iOS_8_0 || PL__PureLayout_MinBaseSDK_OSX_10_10
  67. if ([NSLayoutConstraint respondsToSelector:@selector(deactivateConstraints:)]) {
  68. [NSLayoutConstraint deactivateConstraints:self];
  69. return;
  70. }
  71. #endif /* PL__PureLayout_MinBaseSDK_iOS_8_0 || PL__PureLayout_MinBaseSDK_OSX_10_10 */
  72. for (id object in self) {
  73. if ([object isKindOfClass:[NSLayoutConstraint class]]) {
  74. [((NSLayoutConstraint *)object) autoRemove];
  75. }
  76. }
  77. }
  78. #if PL__PureLayout_MinBaseSDK_iOS_8_0 || PL__PureLayout_MinBaseSDK_OSX_10_10
  79. /**
  80. Sets the string as the identifier for the constraints in this array. Available in iOS 7.0 and OS X 10.9 and later.
  81. The identifier will be printed along with each constraint's description.
  82. This is helpful to document the constraints' purpose and aid in debugging.
  83. @param identifier A string used to identify the constraints in this array.
  84. @return This array.
  85. */
  86. - (instancetype)autoIdentifyConstraints:(NSString *)identifier
  87. {
  88. for (id object in self) {
  89. if ([object isKindOfClass:[NSLayoutConstraint class]]) {
  90. [((NSLayoutConstraint *)object) autoIdentify:identifier];
  91. }
  92. }
  93. return self;
  94. }
  95. #endif /* PL__PureLayout_MinBaseSDK_iOS_8_0 || PL__PureLayout_MinBaseSDK_OSX_10_10 */
  96. #pragma mark Array of Views
  97. /**
  98. Aligns views in this array to one another along a given edge.
  99. Note: This array must contain at least 2 views, and all views must share a common superview.
  100. @param edge The edge to which the views will be aligned.
  101. @return An array of constraints added.
  102. */
  103. - (PL__NSArray_of(NSLayoutConstraint *) *)autoAlignViewsToEdge:(ALEdge)edge
  104. {
  105. NSAssert([self al_containsMinimumNumberOfViews:2], @"This array must contain at least 2 views.");
  106. PL__NSMutableArray_of(NSLayoutConstraint *) *constraints = [NSMutableArray new];
  107. ALView *previousView = nil;
  108. for (id object in self) {
  109. if ([object isKindOfClass:[ALView class]]) {
  110. ALView *view = (ALView *)object;
  111. view.translatesAutoresizingMaskIntoConstraints = NO;
  112. if (previousView) {
  113. [constraints addObject:[view autoPinEdge:edge toEdge:edge ofView:previousView]];
  114. }
  115. previousView = view;
  116. }
  117. }
  118. return constraints;
  119. }
  120. /**
  121. Aligns views in this array to one another along a given axis.
  122. Note: This array must contain at least 2 views, and all views must share a common superview.
  123. @param axis The axis to which the views will be aligned.
  124. @return An array of constraints added.
  125. */
  126. - (PL__NSArray_of(NSLayoutConstraint *) *)autoAlignViewsToAxis:(ALAxis)axis
  127. {
  128. NSAssert([self al_containsMinimumNumberOfViews:2], @"This array must contain at least 2 views.");
  129. PL__NSMutableArray_of(NSLayoutConstraint *) *constraints = [NSMutableArray new];
  130. ALView *previousView = nil;
  131. for (id object in self) {
  132. if ([object isKindOfClass:[ALView class]]) {
  133. ALView *view = (ALView *)object;
  134. view.translatesAutoresizingMaskIntoConstraints = NO;
  135. if (previousView) {
  136. [constraints addObject:[view autoAlignAxis:axis toSameAxisOfView:previousView]];
  137. }
  138. previousView = view;
  139. }
  140. }
  141. return constraints;
  142. }
  143. /**
  144. Matches a given dimension of all the views in this array.
  145. Note: This array must contain at least 2 views, and all views must share a common superview.
  146. @param dimension The dimension to match for all of the views.
  147. @return An array of constraints added.
  148. */
  149. - (PL__NSArray_of(NSLayoutConstraint *) *)autoMatchViewsDimension:(ALDimension)dimension
  150. {
  151. NSAssert([self al_containsMinimumNumberOfViews:2], @"This array must contain at least 2 views.");
  152. PL__NSMutableArray_of(NSLayoutConstraint *) *constraints = [NSMutableArray new];
  153. ALView *previousView = nil;
  154. for (id object in self) {
  155. if ([object isKindOfClass:[ALView class]]) {
  156. ALView *view = (ALView *)object;
  157. view.translatesAutoresizingMaskIntoConstraints = NO;
  158. if (previousView) {
  159. [constraints addObject:[view autoMatchDimension:dimension toDimension:dimension ofView:previousView]];
  160. }
  161. previousView = view;
  162. }
  163. }
  164. return constraints;
  165. }
  166. /**
  167. Sets the given dimension of all the views in this array to a given size.
  168. Note: This array must contain at least 1 view.
  169. @param dimension The dimension of each of the views to set.
  170. @param size The size to set the given dimension of each view to.
  171. @return An array of constraints added.
  172. */
  173. - (PL__NSArray_of(NSLayoutConstraint *) *)autoSetViewsDimension:(ALDimension)dimension toSize:(CGFloat)size
  174. {
  175. NSAssert([self al_containsMinimumNumberOfViews:1], @"This array must contain at least 1 view.");
  176. PL__NSMutableArray_of(NSLayoutConstraint *) *constraints = [NSMutableArray new];
  177. for (id object in self) {
  178. if ([object isKindOfClass:[ALView class]]) {
  179. ALView *view = (ALView *)object;
  180. view.translatesAutoresizingMaskIntoConstraints = NO;
  181. [constraints addObject:[view autoSetDimension:dimension toSize:size]];
  182. }
  183. }
  184. return constraints;
  185. }
  186. /**
  187. Sets all of the views in this array to a given size.
  188. Note: This array must contain at least 1 view.
  189. @param size The size to set each view's dimensions to.
  190. @return An array of constraints added.
  191. */
  192. - (PL__NSArray_of(NSLayoutConstraint *) *)autoSetViewsDimensionsToSize:(CGSize)size
  193. {
  194. PL__NSMutableArray_of(NSLayoutConstraint *) *constraints = [NSMutableArray new];
  195. [constraints addObjectsFromArray:[self autoSetViewsDimension:ALDimensionWidth toSize:size.width]];
  196. [constraints addObjectsFromArray:[self autoSetViewsDimension:ALDimensionHeight toSize:size.height]];
  197. return constraints;
  198. }
  199. /**
  200. Distributes the views in this array equally along the selected axis in their superview.
  201. Views will be the same size (variable) in the dimension along the axis and will have spacing (fixed) between them,
  202. including from the first and last views to their superview.
  203. @param axis The axis along which to distribute the views.
  204. @param alignment The attribute to use to align all the views to one another.
  205. @param spacing The fixed amount of spacing between each view.
  206. @return An array of constraints added.
  207. */
  208. - (PL__NSArray_of(NSLayoutConstraint *) *)autoDistributeViewsAlongAxis:(ALAxis)axis
  209. alignedTo:(ALAttribute)alignment
  210. withFixedSpacing:(CGFloat)spacing
  211. {
  212. return [self autoDistributeViewsAlongAxis:axis
  213. alignedTo:alignment
  214. withFixedSpacing:spacing
  215. insetSpacing:YES];
  216. }
  217. /**
  218. Distributes the views in this array equally along the selected axis in their superview.
  219. Views will be the same size (variable) in the dimension along the axis and will have spacing (fixed) between them.
  220. The first and last views can optionally be inset from their superview by the same amount of spacing as between views.
  221. @param axis The axis along which to distribute the views.
  222. @param alignment The attribute to use to align all the views to one another.
  223. @param spacing The fixed amount of spacing between each view.
  224. @param shouldSpaceInsets Whether the first and last views should be equally inset from their superview.
  225. @return An array of constraints added.
  226. */
  227. - (PL__NSArray_of(NSLayoutConstraint *) *)autoDistributeViewsAlongAxis:(ALAxis)axis
  228. alignedTo:(ALAttribute)alignment
  229. withFixedSpacing:(CGFloat)spacing
  230. insetSpacing:(BOOL)shouldSpaceInsets
  231. {
  232. return [self autoDistributeViewsAlongAxis:axis
  233. alignedTo:alignment
  234. withFixedSpacing:spacing
  235. insetSpacing:shouldSpaceInsets
  236. matchedSizes:YES];
  237. }
  238. /**
  239. Distributes the views in this array equally along the selected axis in their superview.
  240. Views will have fixed spacing between them, and can optionally be constrained to the same size in the dimension along the axis.
  241. The first and last views can optionally be inset from their superview by the same amount of spacing as between views.
  242. @param axis The axis along which to distribute the views.
  243. @param alignment The attribute to use to align all the views to one another.
  244. @param spacing The fixed amount of spacing between each view.
  245. @param shouldSpaceInsets Whether the first and last views should be equally inset from their superview.
  246. @param shouldMatchSizes Whether all views will be constrained to be the same size in the dimension along the axis.
  247. NOTE: All views must specify an intrinsic content size if passing NO, otherwise the layout will be ambiguous!
  248. @return An array of constraints added.
  249. */
  250. - (PL__NSArray_of(NSLayoutConstraint *) *)autoDistributeViewsAlongAxis:(ALAxis)axis
  251. alignedTo:(ALAttribute)alignment
  252. withFixedSpacing:(CGFloat)spacing
  253. insetSpacing:(BOOL)shouldSpaceInsets
  254. matchedSizes:(BOOL)shouldMatchSizes
  255. {
  256. NSAssert([self al_containsMinimumNumberOfViews:1], @"This array must contain at least 1 view to distribute.");
  257. ALDimension matchedDimension;
  258. ALEdge firstEdge, lastEdge;
  259. switch (axis) {
  260. case ALAxisHorizontal:
  261. case ALAxisBaseline: // same value as ALAxisLastBaseline
  262. #if PL__PureLayout_MinBaseSDK_iOS_8_0
  263. case ALAxisFirstBaseline:
  264. #endif /* PL__PureLayout_MinBaseSDK_iOS_8_0 */
  265. matchedDimension = ALDimensionWidth;
  266. firstEdge = ALEdgeLeading;
  267. lastEdge = ALEdgeTrailing;
  268. break;
  269. case ALAxisVertical:
  270. matchedDimension = ALDimensionHeight;
  271. firstEdge = ALEdgeTop;
  272. lastEdge = ALEdgeBottom;
  273. break;
  274. default:
  275. NSAssert(nil, @"Not a valid ALAxis.");
  276. return nil;
  277. }
  278. CGFloat leadingSpacing = shouldSpaceInsets ? spacing : 0.0;
  279. CGFloat trailingSpacing = shouldSpaceInsets ? spacing : 0.0;
  280. PL__NSMutableArray_of(NSLayoutConstraint *) *constraints = [NSMutableArray new];
  281. ALView *previousView = nil;
  282. for (id object in self) {
  283. if ([object isKindOfClass:[ALView class]]) {
  284. ALView *view = (ALView *)object;
  285. view.translatesAutoresizingMaskIntoConstraints = NO;
  286. if (previousView) {
  287. // Second, Third, ... View
  288. [constraints addObject:[view autoPinEdge:firstEdge toEdge:lastEdge ofView:previousView withOffset:spacing]];
  289. if (shouldMatchSizes) {
  290. [constraints addObject:[view autoMatchDimension:matchedDimension toDimension:matchedDimension ofView:previousView]];
  291. }
  292. [constraints addObject:[view al_alignAttribute:alignment toView:previousView forAxis:axis]];
  293. }
  294. else {
  295. // First view
  296. [constraints addObject:[view autoPinEdgeToSuperviewEdge:firstEdge withInset:leadingSpacing]];
  297. }
  298. previousView = view;
  299. }
  300. }
  301. if (previousView) {
  302. // Last View
  303. [constraints addObject:[previousView autoPinEdgeToSuperviewEdge:lastEdge withInset:trailingSpacing]];
  304. }
  305. return constraints;
  306. }
  307. /**
  308. Distributes the views in this array equally along the selected axis in their superview.
  309. Views will be the same size (fixed) in the dimension along the axis and will have spacing (variable) between them,
  310. including from the first and last views to their superview.
  311. @param axis The axis along which to distribute the views.
  312. @param alignment The attribute to use to align all the views to one another.
  313. @param size The fixed size of each view in the dimension along the given axis.
  314. @return An array of constraints added.
  315. */
  316. - (PL__NSArray_of(NSLayoutConstraint *) *)autoDistributeViewsAlongAxis:(ALAxis)axis
  317. alignedTo:(ALAttribute)alignment
  318. withFixedSize:(CGFloat)size
  319. {
  320. return [self autoDistributeViewsAlongAxis:axis
  321. alignedTo:alignment
  322. withFixedSize:size
  323. insetSpacing:YES];
  324. }
  325. /**
  326. Distributes the views in this array equally along the selected axis in their superview.
  327. Views will be the same size (fixed) in the dimension along the axis and will have spacing (variable) between them.
  328. The first and last views can optionally be inset from their superview by the same amount of spacing as between views.
  329. @param axis The axis along which to distribute the views.
  330. @param alignment The attribute to use to align all the views to one another.
  331. @param size The fixed size of each view in the dimension along the given axis.
  332. @param shouldSpaceInsets Whether the first and last views should be equally inset from their superview.
  333. @return An array of constraints added.
  334. */
  335. - (PL__NSArray_of(NSLayoutConstraint *) *)autoDistributeViewsAlongAxis:(ALAxis)axis
  336. alignedTo:(ALAttribute)alignment
  337. withFixedSize:(CGFloat)size
  338. insetSpacing:(BOOL)shouldSpaceInsets
  339. {
  340. NSAssert([self al_containsMinimumNumberOfViews:1], @"This array must contain at least 1 view to distribute.");
  341. ALDimension fixedDimension;
  342. NSLayoutAttribute attribute;
  343. switch (axis) {
  344. case ALAxisHorizontal:
  345. case ALAxisBaseline: // same value as ALAxisLastBaseline
  346. #if PL__PureLayout_MinBaseSDK_iOS_8_0
  347. case ALAxisFirstBaseline:
  348. #endif /* PL__PureLayout_MinBaseSDK_iOS_8_0 */
  349. fixedDimension = ALDimensionWidth;
  350. attribute = NSLayoutAttributeCenterX;
  351. break;
  352. case ALAxisVertical:
  353. fixedDimension = ALDimensionHeight;
  354. attribute = NSLayoutAttributeCenterY;
  355. break;
  356. default:
  357. NSAssert(nil, @"Not a valid ALAxis.");
  358. return nil;
  359. }
  360. #if TARGET_OS_IPHONE
  361. # if !defined(PURELAYOUT_APP_EXTENSIONS)
  362. BOOL isRightToLeftLayout = [[UIApplication sharedApplication] userInterfaceLayoutDirection] == UIUserInterfaceLayoutDirectionRightToLeft;
  363. # else
  364. // App Extensions may not access -[UIApplication sharedApplication]; fall back to checking the bundle's preferred localization character direction
  365. BOOL isRightToLeftLayout = [NSLocale characterDirectionForLanguage:[[NSBundle mainBundle] preferredLocalizations][0]] == NSLocaleLanguageDirectionRightToLeft;
  366. # endif /* !defined(PURELAYOUT_APP_EXTENSIONS) */
  367. #else
  368. BOOL isRightToLeftLayout = [[NSApplication sharedApplication] userInterfaceLayoutDirection] == NSUserInterfaceLayoutDirectionRightToLeft;
  369. #endif /* TARGET_OS_IPHONE */
  370. BOOL shouldFlipOrder = isRightToLeftLayout && (axis != ALAxisVertical); // imitate the effect of leading/trailing when distributing horizontally
  371. PL__NSMutableArray_of(NSLayoutConstraint *) *constraints = [NSMutableArray new];
  372. PL__NSArray_of(ALView *) *views = [self al_copyViewsOnly];
  373. NSUInteger numberOfViews = [views count];
  374. ALView *commonSuperview = [views al_commonSuperviewOfViews];
  375. ALView *previousView = nil;
  376. for (NSUInteger i = 0; i < numberOfViews; i++) {
  377. ALView *view = shouldFlipOrder ? views[numberOfViews - i - 1] : views[i];
  378. view.translatesAutoresizingMaskIntoConstraints = NO;
  379. [constraints addObject:[view autoSetDimension:fixedDimension toSize:size]];
  380. CGFloat multiplier, constant;
  381. if (shouldSpaceInsets) {
  382. multiplier = (i * 2.0 + 2.0) / (numberOfViews + 1.0);
  383. constant = (multiplier - 1.0) * size / 2.0;
  384. } else {
  385. multiplier = (i * 2.0) / (numberOfViews - 1.0);
  386. constant = (-multiplier + 1.0) * size / 2.0;
  387. }
  388. // If the multiplier is very close to 0, set it to the minimum value to prevent the second item in the constraint from being lost. Filed as rdar://19168380
  389. if (fabs(multiplier) < kMULTIPLIER_MIN_VALUE) {
  390. multiplier = kMULTIPLIER_MIN_VALUE;
  391. }
  392. NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:view attribute:attribute relatedBy:NSLayoutRelationEqual toItem:commonSuperview attribute:attribute multiplier:multiplier constant:constant];
  393. [constraint autoInstall];
  394. [constraints addObject:constraint];
  395. if (previousView) {
  396. [constraints addObject:[view al_alignAttribute:alignment toView:previousView forAxis:axis]];
  397. }
  398. previousView = view;
  399. }
  400. return constraints;
  401. }
  402. #pragma mark Internal Helper Methods
  403. /**
  404. Returns the common superview for the views in this array. If there is only one view in the array, its superview will be returned.
  405. Raises an exception if the views in this array do not share a common superview.
  406. @return The common superview for the views in this array.
  407. */
  408. - (ALView *)al_commonSuperviewOfViews
  409. {
  410. ALView *commonSuperview = nil;
  411. ALView *previousView = nil;
  412. for (id object in self) {
  413. if ([object isKindOfClass:[ALView class]]) {
  414. ALView *view = (ALView *)object;
  415. if (previousView) {
  416. commonSuperview = [view al_commonSuperviewWithView:commonSuperview];
  417. } else {
  418. commonSuperview = view.superview;
  419. }
  420. previousView = view;
  421. }
  422. }
  423. NSAssert(commonSuperview, @"Can't constrain views that do not share a common superview. Make sure that all the views in this array have been added into the same view hierarchy.");
  424. return commonSuperview;
  425. }
  426. /**
  427. Determines whether this array contains a minimum number of views.
  428. @param minimumNumberOfViews The minimum number of views to check for.
  429. @return YES if this array contains at least the minimum number of views, NO otherwise.
  430. */
  431. - (BOOL)al_containsMinimumNumberOfViews:(NSUInteger)minimumNumberOfViews
  432. {
  433. NSUInteger numberOfViews = 0;
  434. for (id object in self) {
  435. if ([object isKindOfClass:[ALView class]]) {
  436. numberOfViews++;
  437. if (numberOfViews >= minimumNumberOfViews) {
  438. return YES;
  439. }
  440. }
  441. }
  442. return numberOfViews >= minimumNumberOfViews;
  443. }
  444. /**
  445. Creates a copy of this array containing only the view objects in it.
  446. @return A new array containing only the views that are in this array.
  447. */
  448. - (PL__NSArray_of(ALView *) *)al_copyViewsOnly
  449. {
  450. PL__NSMutableArray_of(ALView *) *viewsOnlyArray = [NSMutableArray arrayWithCapacity:[self count]];
  451. for (id object in self) {
  452. if ([object isKindOfClass:[ALView class]]) {
  453. [viewsOnlyArray addObject:object];
  454. }
  455. }
  456. return viewsOnlyArray;
  457. }
  458. @end