Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68eee74e76 | |||
| 5a4a0f30c4 | |||
| cc2a7348fd | |||
| d89555b9ea | |||
| dc98e270f0 | |||
| e18a72804d | |||
| aa9c4e22a6 | |||
| 1b7f547eda | |||
| dac53d1ac5 | |||
| 5af96519f7 |
@@ -0,0 +1 @@
|
||||
4.2
|
||||
+62
-205
@@ -7,9 +7,7 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
016CC3A1C1D7DDF9EDC5C1162D649B11 /* CollectionSupplement.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC9BCB36774C899E2CFF014515E184DF /* CollectionSupplement.swift */; };
|
||||
02807C6CD1D68F6A8F1D5F5EDBC075F3 /* UIGestureRecognizer+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = B22E8F6CDD346ED32ECF2CF152C7F68F /* UIGestureRecognizer+ext.swift */; };
|
||||
0469A2397AB66C46BE90652653352AB2 /* UICollectionView+DeepDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = D404C95A8030180967F3BA99BECFE580 /* UICollectionView+DeepDiff.swift */; };
|
||||
09471D36F0C65BF373FE54C944C68820 /* StringValidators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73E2BA02088FD493C09D02E47ABE4388 /* StringValidators.swift */; };
|
||||
0BE22BEB5AE38CEC94E0A184C8946969 /* UICollectionView+Reusable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 979016747919997EAF2EC27C458E3C36 /* UICollectionView+Reusable.swift */; };
|
||||
0E0F05E5808AB2414171FE216A4511C6 /* UIView+transitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79E5A847C4EE299FFCC34B5C4BB4354 /* UIView+transitions.swift */; };
|
||||
@@ -23,22 +21,24 @@
|
||||
26DE0E6B75AB5050F5772B922A2AD509 /* UIButton+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FAAEBDF042428F0A388D948B4DF4879 /* UIButton+ext.swift */; };
|
||||
286B06BEE5CF5C9680D6C4E3F7C671D5 /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7D2225A392E110910C049631DFD21A8 /* Reachability.swift */; };
|
||||
29396B3ADC9D79BC235C37FC87898C0E /* Snapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B8F1CF04C636E2B5F6107FCFE6A533 /* Snapshot.swift */; };
|
||||
2ADB6BCE9B619A9A4B87388877977E37 /* Heckel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6EB35A67E9198A6995778F2F1A9FBFD /* Heckel.swift */; };
|
||||
2B4E363F17E50EDA2C69E578CAAE43BF /* utopia-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = ECFF9B3761CE5BD778DBD8E0477703BF /* utopia-dummy.m */; };
|
||||
2F281C3DB35F09EF83172BA7D2A2955E /* UIBarButtonItem+Signals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B1B08AF40BCF36FFC3BF6C3ACEC9B8 /* UIBarButtonItem+Signals.swift */; };
|
||||
2F3FE4ACEF1BBA40DB8FAC3E25915658 /* Toggler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C127A0077D1A43E0B1AF57D5C3E0EC4 /* Toggler.swift */; };
|
||||
30522776ACB49F80FB650434A08771A2 /* KeyboardObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D2E2CE51EEFE3790C4B1336397E3530 /* KeyboardObserver.swift */; };
|
||||
32B0440AA07DD473ABD2D61DF2F3FCD8 /* Literals.swift in Sources */ = {isa = PBXBuildFile; fileRef = A243A5C208AE35F0F0C33004AA1B6620 /* Literals.swift */; };
|
||||
34121FA0A44E5638EF672F112FA7EBCD /* WagnerFischer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90CCD71777EAB46247B48FA67E0B3132 /* WagnerFischer.swift */; };
|
||||
34A0B3185049A34C0C8753721B994638 /* UITextField+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845CCECCD4F6822F1ED13F76CC8FE82 /* UITextField+ext.swift */; };
|
||||
35937A6BCDC0AD740B2768CF56F18A0A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6604A7D69453B4569E4E4827FB9155A9 /* Foundation.framework */; };
|
||||
35BB5E12A394D82FF0F8E675F3000FDB /* Math.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CADBB7F40CC7CCB173E2090A583C51 /* Math.swift */; };
|
||||
35F36F6F8769CDC19189C6C44BECE03E /* SignalSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AFAA0B21453AB7FAEF276D19B10890A /* SignalSubscription.swift */; };
|
||||
391DB7CE215316AB0083B8C3 /* HorizontalAlignmentLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 391DB7CD215316AB0083B8C3 /* HorizontalAlignmentLayout.swift */; };
|
||||
391DB7D0215316F50083B8C3 /* InteractiveDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = 391DB7CF215316F50083B8C3 /* InteractiveDismiss.swift */; };
|
||||
391DB7D4215317990083B8C3 /* GestureRecognizerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 391DB7D3215317990083B8C3 /* GestureRecognizerDelegate.swift */; };
|
||||
391DB7D6215317A20083B8C3 /* TouchInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 391DB7D5215317A20083B8C3 /* TouchInteraction.swift */; };
|
||||
3948F8B92193A3000043FD2E /* PinnedHeaderFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3948F8B82193A3000043FD2E /* PinnedHeaderFlowLayout.swift */; };
|
||||
398EF3F919D8AFA5093B15156F27E353 /* AssociatedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90027142D498AD8ABA5215ADC06AC80 /* AssociatedObject.swift */; };
|
||||
3C087E90B6D5631B925B524FBD183DA5 /* Associated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A88C1BE5A50F7B80FB3A7EF1DD75D5 /* Associated.swift */; };
|
||||
3C593180EE447D73A5ADBA3AE8909897 /* InputVisibilityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C3D37E1C6C9537466FBA3F006E1A2D /* InputVisibilityController.swift */; };
|
||||
40077E1D3AAA704AC97E1AEDA22CF464 /* StatusBarAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C4EEBAEB1E1FDB6EDA95CFF0B7ECFA7 /* StatusBarAppearance.swift */; };
|
||||
46F572A824B37693AF514E85E0549C58 /* DictionaryErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09EDFCC54FF8EC7D7F64E9001D66CD11 /* DictionaryErrors.swift */; };
|
||||
4837A82C9004D6D819B45977200EEF92 /* Haptic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DAD12A7E3A6444E9DD66F5B3EF62046 /* Haptic.swift */; };
|
||||
4F58D683476B2C93625C121FAC487769 /* CoreAnimation+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB7054EF292E6C6EDA81B3B7AC3586 /* CoreAnimation+ext.swift */; };
|
||||
50202062FF2FEFA5A435C025B28E6F2E /* LayoutGuides.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA90010E69BA8984268435FC0BC4C19F /* LayoutGuides.swift */; };
|
||||
@@ -47,48 +47,33 @@
|
||||
5533FA61D479D4E52F22B6D7E5634127 /* Signal.swift in Sources */ = {isa = PBXBuildFile; fileRef = C78E8030A43AACA23F8A7A60874912D6 /* Signal.swift */; };
|
||||
556DBB6B22CB0D57352504204588353C /* SwiftyImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E51ADD4779D6AC8AEB0E4AC214241 /* SwiftyImageView.swift */; };
|
||||
56AFDBABD2F98A47878F3C6D24D2C18D /* UISearchUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A7A173B2BB4912B56024A0AB99C624 /* UISearchUtilities.swift */; };
|
||||
57436A3B37C2DDBE9953C104FB706A81 /* DictionaryEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3F60E978A7284B439BE2FCDE3600871 /* DictionaryEncoder.swift */; };
|
||||
5788D3A4E388EEC282525FA728EFFD4B /* CollectionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7821B811634043201CB58B8B98018ED5 /* CollectionItem.swift */; };
|
||||
58D9DE8371AC03A18432CDD1DC4C1FDB /* Pods-utopia_Example-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC125537CD9723ABCD3B1A86A59DF34 /* Pods-utopia_Example-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
5B190B9D09C4A639DA32D00681E1B4A7 /* Pods-utopia_Example-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = B9483E800344D13D356F1D83D85F3E8D /* Pods-utopia_Example-dummy.m */; };
|
||||
5F10FDD4882A344EBC6F15A2A7824F21 /* UITableView+DeepDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DE0C9BA6C35A8CBF0101B1C408D1CC8 /* UITableView+DeepDiff.swift */; };
|
||||
6043ADCB4A6A8BF4E704C73292A5F4FE /* CollectionItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D8ADED2A9CC7AC64300CA0741531748 /* CollectionItemProtocol.swift */; };
|
||||
696FBE84EEB2A8F325E8D551903BF524 /* UIControl+Signals.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B061582844CAED75F90A2B4DF96BBF /* UIControl+Signals.swift */; };
|
||||
728E7F3E40B77C6AE1F2EA03435547F9 /* CodableUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9BB03A7F405472718898E3CF28F4BF /* CodableUtilities.swift */; };
|
||||
737F4D68613986033F0FE3870FFD20D3 /* DictionaryCodingKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0737DBB484F81C7B892CB23EBFFDEF86 /* DictionaryCodingKey.swift */; };
|
||||
73B0A6F48F64287FDB4E768D7647E352 /* UINavigationController+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE531C30779681DEBE1F6FF256BD3F89 /* UINavigationController+ext.swift */; };
|
||||
75C03B94D42A4B2C7871A6607BB8E2D6 /* CollectionFlowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C60770A293C32AF6DFEF8BD9597843 /* CollectionFlowDelegate.swift */; };
|
||||
76E15EE6FF172907EC6293F7C471A7ED /* DiffAware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E215442310E056A1978EA9D5760C1D4 /* DiffAware.swift */; };
|
||||
77E60E92F55D44D77B524CE6712BB6FA /* SingleCellController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B7BDB142DD281BDC64282677DC85EAA /* SingleCellController.swift */; };
|
||||
826398D12955504DC62BF1167ED6D605 /* Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9138D045BD09D068F3E99C8C8C6D9A52 /* Display.swift */; };
|
||||
854C49070D0FE89878C8E5D0124CD65E /* ScrollDismission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507F6CFD78D079631EF4C7414D722BE9 /* ScrollDismission.swift */; };
|
||||
8CBEFDF21CBA10F3358929B79D9A29B5 /* UIStackView+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC51A8E479CDC53438411C9DC52FACE /* UIStackView+ext.swift */; };
|
||||
9052FD4E4A2BF4D796D2521EE47B2069 /* UIButton+layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FEFFB563FD2B0AD812EE3011859DACD /* UIButton+layout.swift */; };
|
||||
93C8DEBCE66F72F7E1F2C56C3C48F212 /* CoreGraphics+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C2F48088F779A8F8FD0D3AB796A081B /* CoreGraphics+ext.swift */; };
|
||||
999AFF8B082890847D8ED1EDF59F8BEE /* DeepDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FAB1B98BE9D96E1701AA0892C18E5B8 /* DeepDiff.swift */; };
|
||||
9B702BEA0A412AE61A2A100047A2682A /* ImageSettable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 655F593EC54CC0CC958E764E3A09282D /* ImageSettable.swift */; };
|
||||
9E1484918CFB62F8A4DBAFD2604BDBBC /* Require.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3519B7CEB0403B5FAF2E8610A97B1501 /* Require.swift */; };
|
||||
9E5606121D34743FE6F4D34A4A5A0704 /* UIViewController+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BE5E5B9BC9342D12093473CC3AAD147 /* UIViewController+ext.swift */; };
|
||||
A064E2905C6E6EDBB8A956E45B6E2847 /* UIView+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F184EF2BEEE4AD727A781280D50E64 /* UIView+ext.swift */; };
|
||||
A2591F809F08E89B6AD7B6DA7561154D /* CollectionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BE0503B04700BC34A6A721CDE0F749 /* CollectionSection.swift */; };
|
||||
A3DA1CA55FB34B50280E9E24460A7C50 /* Then.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7478349DD7D5B359C7BDCE6B82A1658 /* Then.swift */; };
|
||||
A4D0DE6907F75000EFA2D909D97426DA /* UITableView+Reusable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41498A7B2FB6C962AF2CAA1727FA4BEE /* UITableView+Reusable.swift */; };
|
||||
A5EC34CCDF7A16CDB83E879951608D29 /* CollectionSupplementProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3962543B622CFDF60B27C1E57A8EB2DC /* CollectionSupplementProtocol.swift */; };
|
||||
A848BE440EAD8450C7B15BE5DB31BAE7 /* String+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2149259B5AE77BCB89C40EA8AE51FAF8 /* String+ext.swift */; };
|
||||
A956D28489AB7AB9B6DCF30C903F3E73 /* IndexPathConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E135A4AEFE85701CBF9E0A8FB4DA413 /* IndexPathConverter.swift */; };
|
||||
B4634EB2195A74E5705E2EB5153D3961 /* Collections+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBAC24B57F8D1BC9EF7FD1B9338F8465 /* Collections+Extensions.swift */; };
|
||||
B958EFF6259E0EE099176C57F09CEC4D /* UIImage+Gradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7BDDF72B5E7808085AF42B49A8E4304 /* UIImage+Gradient.swift */; };
|
||||
B9E8F5741335FF9544757AEEFA063C4C /* UIAlertController+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E58A77B34639C4325DEBB0B8BBCB52 /* UIAlertController+ext.swift */; };
|
||||
BD69427FEADE03F3A2931FDE6A90B28C /* UIScrollView+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = F19BE43ABB3F8A8B56A4DD5F038130D2 /* UIScrollView+ext.swift */; };
|
||||
C1EA2BF250F881A8CE274F56698CA5ED /* Hapticable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 352DCA45D28C00C59D68C1E66313DA94 /* Hapticable.swift */; };
|
||||
C5B3A325A391083BCBE0225D0E492651 /* AlphaTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F563779A49E6F8BABD9F7F096FAE38 /* AlphaTransition.swift */; };
|
||||
C980A8B4B7D46F918161A80D523F5B9A /* Comparable+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF39CDC694A39ABC8161159EFBF1969 /* Comparable+ext.swift */; };
|
||||
CA40B44897ADD77342DE139BB3D226F0 /* DictionaryDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540BECA9B3C663E6711DAB28887C3F18 /* DictionaryDecoder.swift */; };
|
||||
CA706B65DFD60F03EDC1773828AC4306 /* UIColor+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002CE560EE32DF3EA2939698D55CA6DC /* UIColor+ext.swift */; };
|
||||
CA834F710C7B8DE7E78756369DB8B082 /* DateUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD719201D9EBBBFBC9D36540F14DFA52 /* DateUtilities.swift */; };
|
||||
CAF36258A097CBCB956B2742329293F7 /* SwiftyColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C7669A602A5ACC46EB4D109E37EBBD /* SwiftyColor.swift */; };
|
||||
D5136D58B70ED60743594DDA623340A6 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B2707546E60D2905D4AF46DB62253F2 /* Result.swift */; };
|
||||
D57151A2D17D6AA1A791C365350D6637 /* IndexSetConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4AE276360C5D697A6FE49EC5966A25 /* IndexSetConverter.swift */; };
|
||||
D7D7B6285FF6691B27A7D73FF196468A /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 325E227EFA7126E2C5DD6AD776413857 /* Bundle.swift */; };
|
||||
D91F78D2FCE584E9E3E945F8FDD0C80E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6604A7D69453B4569E4E4827FB9155A9 /* Foundation.framework */; };
|
||||
D968A8500BD1AC4EC3FD58F23FCD6D43 /* Collections+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C8786B90BD56FD05CE4783E0C4376AA /* Collections+ext.swift */; };
|
||||
@@ -97,13 +82,9 @@
|
||||
E5B2024891F21779A4AB81AB91A2A03D /* UIImage+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F598BCDB420F275A7F1F43F9FC7C90 /* UIImage+ext.swift */; };
|
||||
EAF44B11790232341E89C0702F35AE13 /* ValidatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 187CAF707C37B678EAE174AAD0E9A9A0 /* ValidatableView.swift */; };
|
||||
EB0A1625ED2019436ED4AC46F426F3DA /* TimingFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5615B6D865408D6F06B2F71F6FA57D /* TimingFunction.swift */; };
|
||||
ECB21B60D35987F9AFFEC942F656D70B /* CollectionDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617AD57D60956AE03CF1A749AC9EAC63 /* CollectionDriver.swift */; };
|
||||
F0A66F0F3A522BECA0BCFE8EAB863330 /* CollectionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C4DD617686BC4EFC847C517E9D795F /* CollectionDelegate.swift */; };
|
||||
F1C2819748E2528F3DC497BD3B369EB2 /* Change.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7209A4A336A5EED90B1CAFE2F60EF734 /* Change.swift */; };
|
||||
F40750FA2D57A01A96A9EED6AF586FF9 /* TouchRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97F35D35AEE8986797BCB7527E30A0FA /* TouchRecognizer.swift */; };
|
||||
F40A972F66F6674CDEA0187860E8785B /* ScaleModalTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7B5BD275A40EA5A7887E4CDB8A5084 /* ScaleModalTransitioning.swift */; };
|
||||
F844B62463F01BDF65C1553CD6670CC2 /* Random.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93708F44C72C4C524438FB78D87F11F2 /* Random.swift */; };
|
||||
F8907750D7B78472C381EA587CA1DDE8 /* MoveReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E49B104F3AB6CF2FA7145DAD7073C5 /* MoveReducer.swift */; };
|
||||
FBA69FEF5BF5077D1A63854B7286CC57 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 240C5CEE131CBCE6AA722022C094268E /* AsyncOperation.swift */; };
|
||||
FC73128B984C0755DA32C1E1A15A5DBB /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4F37218272562786A1EEC14A219F1CB /* GradientView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
@@ -122,17 +103,12 @@
|
||||
002CE560EE32DF3EA2939698D55CA6DC /* UIColor+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIColor+ext.swift"; sourceTree = "<group>"; };
|
||||
03B1B68A1207D9BA0DEC85B5917051B3 /* Pods-utopia_Example-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-utopia_Example-acknowledgements.markdown"; sourceTree = "<group>"; };
|
||||
04B8F1CF04C636E2B5F6107FCFE6A533 /* Snapshot.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Snapshot.swift; sourceTree = "<group>"; };
|
||||
0737DBB484F81C7B892CB23EBFFDEF86 /* DictionaryCodingKey.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DictionaryCodingKey.swift; sourceTree = "<group>"; };
|
||||
09EDFCC54FF8EC7D7F64E9001D66CD11 /* DictionaryErrors.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DictionaryErrors.swift; sourceTree = "<group>"; };
|
||||
0B2707546E60D2905D4AF46DB62253F2 /* Result.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = "<group>"; };
|
||||
0DE0C9BA6C35A8CBF0101B1C408D1CC8 /* UITableView+DeepDiff.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UITableView+DeepDiff.swift"; sourceTree = "<group>"; };
|
||||
0E215442310E056A1978EA9D5760C1D4 /* DiffAware.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DiffAware.swift; sourceTree = "<group>"; };
|
||||
0FEFFB563FD2B0AD812EE3011859DACD /* UIButton+layout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIButton+layout.swift"; sourceTree = "<group>"; };
|
||||
1823695516669A365A0F87CA37208712 /* Optional+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "Optional+ext.swift"; sourceTree = "<group>"; };
|
||||
187CAF707C37B678EAE174AAD0E9A9A0 /* ValidatableView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ValidatableView.swift; sourceTree = "<group>"; };
|
||||
1C2142D6AC34C79CCB601E097A9C3922 /* BarButtons.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BarButtons.swift; sourceTree = "<group>"; };
|
||||
1DF39CDC694A39ABC8161159EFBF1969 /* Comparable+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "Comparable+ext.swift"; sourceTree = "<group>"; };
|
||||
1E135A4AEFE85701CBF9E0A8FB4DA413 /* IndexPathConverter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = IndexPathConverter.swift; sourceTree = "<group>"; };
|
||||
2149259B5AE77BCB89C40EA8AE51FAF8 /* String+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "String+ext.swift"; sourceTree = "<group>"; };
|
||||
240C5CEE131CBCE6AA722022C094268E /* AsyncOperation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AsyncOperation.swift; sourceTree = "<group>"; };
|
||||
24E58A77B34639C4325DEBB0B8BBCB52 /* UIAlertController+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIAlertController+ext.swift"; sourceTree = "<group>"; };
|
||||
@@ -144,12 +120,13 @@
|
||||
32E405E677F5BBAF94C423F133E9F257 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
3519B7CEB0403B5FAF2E8610A97B1501 /* Require.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Require.swift; sourceTree = "<group>"; };
|
||||
352DCA45D28C00C59D68C1E66313DA94 /* Hapticable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Hapticable.swift; sourceTree = "<group>"; };
|
||||
3962543B622CFDF60B27C1E57A8EB2DC /* CollectionSupplementProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CollectionSupplementProtocol.swift; sourceTree = "<group>"; };
|
||||
3B7BDB142DD281BDC64282677DC85EAA /* SingleCellController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SingleCellController.swift; sourceTree = "<group>"; };
|
||||
3FAB1B98BE9D96E1701AA0892C18E5B8 /* DeepDiff.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeepDiff.swift; sourceTree = "<group>"; };
|
||||
391DB7CD215316AB0083B8C3 /* HorizontalAlignmentLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalAlignmentLayout.swift; sourceTree = "<group>"; };
|
||||
391DB7CF215316F50083B8C3 /* InteractiveDismiss.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InteractiveDismiss.swift; sourceTree = "<group>"; };
|
||||
391DB7D3215317990083B8C3 /* GestureRecognizerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GestureRecognizerDelegate.swift; sourceTree = "<group>"; };
|
||||
391DB7D5215317A20083B8C3 /* TouchInteraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchInteraction.swift; sourceTree = "<group>"; };
|
||||
3948F8B82193A3000043FD2E /* PinnedHeaderFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedHeaderFlowLayout.swift; sourceTree = "<group>"; };
|
||||
41498A7B2FB6C962AF2CAA1727FA4BEE /* UITableView+Reusable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UITableView+Reusable.swift"; sourceTree = "<group>"; };
|
||||
43A88C1BE5A50F7B80FB3A7EF1DD75D5 /* Associated.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Associated.swift; sourceTree = "<group>"; };
|
||||
45C60770A293C32AF6DFEF8BD9597843 /* CollectionFlowDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CollectionFlowDelegate.swift; sourceTree = "<group>"; };
|
||||
474B741D8D941D2368F81BD923718887 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
485554E3BDF22424E17D04F2C017B0C9 /* SwiftyImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwiftyImage.swift; sourceTree = "<group>"; };
|
||||
496E51ADD4779D6AC8AEB0E4AC214241 /* SwiftyImageView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwiftyImageView.swift; sourceTree = "<group>"; };
|
||||
@@ -159,45 +136,36 @@
|
||||
507F6CFD78D079631EF4C7414D722BE9 /* ScrollDismission.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ScrollDismission.swift; sourceTree = "<group>"; };
|
||||
51A7A173B2BB4912B56024A0AB99C624 /* UISearchUtilities.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UISearchUtilities.swift; sourceTree = "<group>"; };
|
||||
53845654F739F2E9534B24DEC5A577AD /* SimpleError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SimpleError.swift; sourceTree = "<group>"; };
|
||||
540BECA9B3C663E6711DAB28887C3F18 /* DictionaryDecoder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DictionaryDecoder.swift; sourceTree = "<group>"; };
|
||||
55C7669A602A5ACC46EB4D109E37EBBD /* SwiftyColor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwiftyColor.swift; sourceTree = "<group>"; };
|
||||
58C4DD617686BC4EFC847C517E9D795F /* CollectionDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CollectionDelegate.swift; sourceTree = "<group>"; };
|
||||
5C127A0077D1A43E0B1AF57D5C3E0EC4 /* Toggler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Toggler.swift; sourceTree = "<group>"; };
|
||||
5D8ADED2A9CC7AC64300CA0741531748 /* CollectionItemProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CollectionItemProtocol.swift; sourceTree = "<group>"; };
|
||||
617AD57D60956AE03CF1A749AC9EAC63 /* CollectionDriver.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CollectionDriver.swift; sourceTree = "<group>"; };
|
||||
655F593EC54CC0CC958E764E3A09282D /* ImageSettable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImageSettable.swift; sourceTree = "<group>"; };
|
||||
6604A7D69453B4569E4E4827FB9155A9 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.3.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||
66469B3F430396881B1F26EDD4EB8C0C /* utopia.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; lastKnownFileType = text; path = utopia.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
|
||||
66469B3F430396881B1F26EDD4EB8C0C /* utopia.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; path = utopia.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
|
||||
6C9C018F9DF21B16BCBA1BD49027615B /* TransitioningDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransitioningDelegate.swift; sourceTree = "<group>"; };
|
||||
7209A4A336A5EED90B1CAFE2F60EF734 /* Change.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Change.swift; sourceTree = "<group>"; };
|
||||
73E2BA02088FD493C09D02E47ABE4388 /* StringValidators.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StringValidators.swift; sourceTree = "<group>"; };
|
||||
73F563779A49E6F8BABD9F7F096FAE38 /* AlphaTransition.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AlphaTransition.swift; sourceTree = "<group>"; };
|
||||
7821B811634043201CB58B8B98018ED5 /* CollectionItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CollectionItem.swift; sourceTree = "<group>"; };
|
||||
7C2F48088F779A8F8FD0D3AB796A081B /* CoreGraphics+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "CoreGraphics+ext.swift"; sourceTree = "<group>"; };
|
||||
7FAAEBDF042428F0A388D948B4DF4879 /* UIButton+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIButton+ext.swift"; sourceTree = "<group>"; };
|
||||
80C3D37E1C6C9537466FBA3F006E1A2D /* InputVisibilityController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = InputVisibilityController.swift; sourceTree = "<group>"; };
|
||||
82E49B104F3AB6CF2FA7145DAD7073C5 /* MoveReducer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MoveReducer.swift; sourceTree = "<group>"; };
|
||||
85C649278AE9FEC1B162C60ABAAE5123 /* Time.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Time.swift; sourceTree = "<group>"; };
|
||||
8652424025F661444A921AE66149ED15 /* utopia.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = utopia.modulemap; sourceTree = "<group>"; };
|
||||
8BE5E5B9BC9342D12093473CC3AAD147 /* UIViewController+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIViewController+ext.swift"; sourceTree = "<group>"; };
|
||||
8C4EEBAEB1E1FDB6EDA95CFF0B7ECFA7 /* StatusBarAppearance.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StatusBarAppearance.swift; sourceTree = "<group>"; };
|
||||
8C8786B90BD56FD05CE4783E0C4376AA /* Collections+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "Collections+ext.swift"; sourceTree = "<group>"; };
|
||||
90CCD71777EAB46247B48FA67E0B3132 /* WagnerFischer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WagnerFischer.swift; sourceTree = "<group>"; };
|
||||
9138D045BD09D068F3E99C8C8C6D9A52 /* Display.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Display.swift; sourceTree = "<group>"; };
|
||||
93708F44C72C4C524438FB78D87F11F2 /* Random.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Random.swift; sourceTree = "<group>"; };
|
||||
93A4A3777CF96A4AAC1D13BA6DCCEA73 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
|
||||
93A4A3777CF96A4AAC1D13BA6DCCEA73 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
|
||||
95B1B08AF40BCF36FFC3BF6C3ACEC9B8 /* UIBarButtonItem+Signals.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+Signals.swift"; sourceTree = "<group>"; };
|
||||
979016747919997EAF2EC27C458E3C36 /* UICollectionView+Reusable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Reusable.swift"; sourceTree = "<group>"; };
|
||||
97F35D35AEE8986797BCB7527E30A0FA /* TouchRecognizer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TouchRecognizer.swift; sourceTree = "<group>"; };
|
||||
9C0BAFFF44BDDD1F5D3F75989ECECCDA /* Pods_utopia_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_utopia_Example.framework; path = "Pods-utopia_Example.framework"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9C1B520DA013589C85626024C3113C85 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; path = LICENSE; sourceTree = "<group>"; };
|
||||
9C0BAFFF44BDDD1F5D3F75989ECECCDA /* Pods_utopia_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_utopia_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9C1B520DA013589C85626024C3113C85 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
||||
9F7B5BD275A40EA5A7887E4CDB8A5084 /* ScaleModalTransitioning.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ScaleModalTransitioning.swift; sourceTree = "<group>"; };
|
||||
9F9BB03A7F405472718898E3CF28F4BF /* CodableUtilities.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CodableUtilities.swift; sourceTree = "<group>"; };
|
||||
9FC51A8E479CDC53438411C9DC52FACE /* UIStackView+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIStackView+ext.swift"; sourceTree = "<group>"; };
|
||||
A054AB447DAE64070D4411D2FBF7A04B /* Pods-utopia_Example.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-utopia_Example.modulemap"; sourceTree = "<group>"; };
|
||||
A1F184EF2BEEE4AD727A781280D50E64 /* UIView+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIView+ext.swift"; sourceTree = "<group>"; };
|
||||
A243A5C208AE35F0F0C33004AA1B6620 /* Literals.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Literals.swift; sourceTree = "<group>"; };
|
||||
A3F60E978A7284B439BE2FCDE3600871 /* DictionaryEncoder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DictionaryEncoder.swift; sourceTree = "<group>"; };
|
||||
A4F37218272562786A1EEC14A219F1CB /* GradientView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = "<group>"; };
|
||||
A9CADBB7F40CC7CCB173E2090A583C51 /* Math.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Math.swift; sourceTree = "<group>"; };
|
||||
AD719201D9EBBBFBC9D36540F14DFA52 /* DateUtilities.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DateUtilities.swift; sourceTree = "<group>"; };
|
||||
@@ -206,34 +174,28 @@
|
||||
B7478349DD7D5B359C7BDCE6B82A1658 /* Then.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Then.swift; sourceTree = "<group>"; };
|
||||
B7D2225A392E110910C049631DFD21A8 /* Reachability.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Reachability.swift; sourceTree = "<group>"; };
|
||||
B9483E800344D13D356F1D83D85F3E8D /* Pods-utopia_Example-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-utopia_Example-dummy.m"; sourceTree = "<group>"; };
|
||||
BAC1F58768C710A3C7F6F45E94924CCC /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; path = README.md; sourceTree = "<group>"; };
|
||||
BC9BCB36774C899E2CFF014515E184DF /* CollectionSupplement.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CollectionSupplement.swift; sourceTree = "<group>"; };
|
||||
BAC1F58768C710A3C7F6F45E94924CCC /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
BE531C30779681DEBE1F6FF256BD3F89 /* UINavigationController+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UINavigationController+ext.swift"; sourceTree = "<group>"; };
|
||||
C0F598BCDB420F275A7F1F43F9FC7C90 /* UIImage+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIImage+ext.swift"; sourceTree = "<group>"; };
|
||||
C78E8030A43AACA23F8A7A60874912D6 /* Signal.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Signal.swift; sourceTree = "<group>"; };
|
||||
C7BDDF72B5E7808085AF42B49A8E4304 /* UIImage+Gradient.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIImage+Gradient.swift"; sourceTree = "<group>"; };
|
||||
CB675E040FD2E33E8569104B5F66CE0F /* NSAttributedStrings+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "NSAttributedStrings+ext.swift"; sourceTree = "<group>"; };
|
||||
CDC125537CD9723ABCD3B1A86A59DF34 /* Pods-utopia_Example-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-utopia_Example-umbrella.h"; sourceTree = "<group>"; };
|
||||
D0BE0503B04700BC34A6A721CDE0F749 /* CollectionSection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CollectionSection.swift; sourceTree = "<group>"; };
|
||||
D3453900FDF815600817E368E63B0D82 /* Pods-utopia_Example-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-utopia_Example-resources.sh"; sourceTree = "<group>"; };
|
||||
D404C95A8030180967F3BA99BECFE580 /* UICollectionView+DeepDiff.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UICollectionView+DeepDiff.swift"; sourceTree = "<group>"; };
|
||||
D40A7DF8EA44630F470D2EFA45276B2F /* Pods-utopia_Example-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-utopia_Example-frameworks.sh"; sourceTree = "<group>"; };
|
||||
D845CCECCD4F6822F1ED13F76CC8FE82 /* UITextField+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UITextField+ext.swift"; sourceTree = "<group>"; };
|
||||
E56C9C357A8A21ED85640622889CA007 /* Pods-utopia_Example-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-utopia_Example-acknowledgements.plist"; sourceTree = "<group>"; };
|
||||
E6EB35A67E9198A6995778F2F1A9FBFD /* Heckel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Heckel.swift; sourceTree = "<group>"; };
|
||||
EBAC24B57F8D1BC9EF7FD1B9338F8465 /* Collections+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "Collections+Extensions.swift"; sourceTree = "<group>"; };
|
||||
ECFF9B3761CE5BD778DBD8E0477703BF /* utopia-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "utopia-dummy.m"; sourceTree = "<group>"; };
|
||||
EF4F6D046DADE0DD3A4E045F76D1CE16 /* UIWindow+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIWindow+ext.swift"; sourceTree = "<group>"; };
|
||||
F19BE43ABB3F8A8B56A4DD5F038130D2 /* UIScrollView+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIScrollView+ext.swift"; sourceTree = "<group>"; };
|
||||
F2AB7054EF292E6C6EDA81B3B7AC3586 /* CoreAnimation+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "CoreAnimation+ext.swift"; sourceTree = "<group>"; };
|
||||
F3B061582844CAED75F90A2B4DF96BBF /* UIControl+Signals.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIControl+Signals.swift"; sourceTree = "<group>"; };
|
||||
F4006CD46651420CC284F64C2BB3E679 /* utopia.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = utopia.framework; path = utopia.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F4006CD46651420CC284F64C2BB3E679 /* utopia.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = utopia.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F61C18A1761D447D4249ABA4E14D11AE /* Pods-utopia_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-utopia_Example.release.xcconfig"; sourceTree = "<group>"; };
|
||||
F79E5A847C4EE299FFCC34B5C4BB4354 /* UIView+transitions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIView+transitions.swift"; sourceTree = "<group>"; };
|
||||
F90027142D498AD8ABA5215ADC06AC80 /* AssociatedObject.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssociatedObject.swift; sourceTree = "<group>"; };
|
||||
FA90010E69BA8984268435FC0BC4C19F /* LayoutGuides.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LayoutGuides.swift; sourceTree = "<group>"; };
|
||||
FAFC54380DC5D0CA56A4551D0E7806AE /* Pods-utopia_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-utopia_Example.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
FD4AE276360C5D697A6FE49EC5966A25 /* IndexSetConverter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = IndexSetConverter.swift; sourceTree = "<group>"; };
|
||||
FE5615B6D865408D6F06B2F71F6FA57D /* TimingFunction.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TimingFunction.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -274,34 +236,9 @@
|
||||
children = (
|
||||
A4F37218272562786A1EEC14A219F1CB /* GradientView.swift */,
|
||||
);
|
||||
name = Views;
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C664FBE14B49F100468748D16E8AA36 /* DictionaryCoding */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0737DBB484F81C7B892CB23EBFFDEF86 /* DictionaryCodingKey.swift */,
|
||||
540BECA9B3C663E6711DAB28887C3F18 /* DictionaryDecoder.swift */,
|
||||
A3F60E978A7284B439BE2FCDE3600871 /* DictionaryEncoder.swift */,
|
||||
09EDFCC54FF8EC7D7F64E9001D66CD11 /* DictionaryErrors.swift */,
|
||||
);
|
||||
name = DictionaryCoding;
|
||||
path = DictionaryCoding;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1709A2FF999AEA557856138B4BEE53A4 /* iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1E135A4AEFE85701CBF9E0A8FB4DA413 /* IndexPathConverter.swift */,
|
||||
FD4AE276360C5D697A6FE49EC5966A25 /* IndexSetConverter.swift */,
|
||||
D404C95A8030180967F3BA99BECFE580 /* UICollectionView+DeepDiff.swift */,
|
||||
0DE0C9BA6C35A8CBF0101B1C408D1CC8 /* UITableView+DeepDiff.swift */,
|
||||
);
|
||||
name = iOS;
|
||||
path = iOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2656CB43AC1C0394F9AE8BB2C3FDA13C /* Pods-utopia_Example */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -320,15 +257,6 @@
|
||||
path = "Target Support Files/Pods-utopia_Example";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
27E0941C66C0AE0979F23F42144634D6 /* Section */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0BE0503B04700BC34A6A721CDE0F749 /* CollectionSection.swift */,
|
||||
);
|
||||
name = Section;
|
||||
path = Section;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
290A9D5AED8BFBC4A9E64E0F9ED12AE3 /* Pod */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -339,35 +267,46 @@
|
||||
name = Pod;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
298C0019EBCB51F394F9FBF6A237AF84 /* Shared */ = {
|
||||
391DB7CC2153164E0083B8C3 /* CollectionLayout */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7209A4A336A5EED90B1CAFE2F60EF734 /* Change.swift */,
|
||||
EBAC24B57F8D1BC9EF7FD1B9338F8465 /* Collections+Extensions.swift */,
|
||||
3FAB1B98BE9D96E1701AA0892C18E5B8 /* DeepDiff.swift */,
|
||||
82E49B104F3AB6CF2FA7145DAD7073C5 /* MoveReducer.swift */,
|
||||
EBBAD62BE8FC301DB475A5815674E8B8 /* Algorithms */,
|
||||
3948F8B82193A3000043FD2E /* PinnedHeaderFlowLayout.swift */,
|
||||
391DB7CD215316AB0083B8C3 /* HorizontalAlignmentLayout.swift */,
|
||||
);
|
||||
name = Shared;
|
||||
path = Shared;
|
||||
path = CollectionLayout;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
391DB7D1215317290083B8C3 /* Animations */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
391DB7D5215317A20083B8C3 /* TouchInteraction.swift */,
|
||||
);
|
||||
path = Animations;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
391DB7D2215317760083B8C3 /* Gestures */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
391DB7D3215317990083B8C3 /* GestureRecognizerDelegate.swift */,
|
||||
);
|
||||
path = Gestures;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C85535653D978EAFC4C56A439ADA3FA /* utopia */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AF519AAB88449C069E4444CD0A7D84A8 /* CodingDeconding */,
|
||||
8241AC4FBF88BD92580122D1ADF6D6E4 /* CollectionDriver */,
|
||||
93DB251A38E863705E10AF7CBB6CF7D7 /* Haptic */,
|
||||
01B7BFB7353AE9D18262AD05EA941F21 /* OptimizationTricks */,
|
||||
290A9D5AED8BFBC4A9E64E0F9ED12AE3 /* Pod */,
|
||||
768C46A62A04556281811B75E933B353 /* Signals */,
|
||||
8C27DC0C74C30233B185E75F27845D0A /* Support Files */,
|
||||
D9197775FFD136C39DC9830BA734884F /* SwiftStdLib+ext */,
|
||||
C27A836CBE9528008AEACE449CAEE29E /* Transitions */,
|
||||
A2D72D5D8ACADAD2D6EB22A8DE9C1415 /* Types */,
|
||||
BAA834FD327249945A0B7BD0F2560561 /* UIKit */,
|
||||
CB87AC1D6A6B7C7CC462ED8CAA7B02A3 /* Utilities */,
|
||||
7595EBA1F3F4CD4ECB8D3C14E5C0B1A6 /* Validation */,
|
||||
8C27DC0C74C30233B185E75F27845D0A /* Support Files */,
|
||||
290A9D5AED8BFBC4A9E64E0F9ED12AE3 /* Pod */,
|
||||
);
|
||||
name = utopia;
|
||||
path = ../..;
|
||||
@@ -425,30 +364,6 @@
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8241AC4FBF88BD92580122D1ADF6D6E4 /* CollectionDriver */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
58C4DD617686BC4EFC847C517E9D795F /* CollectionDelegate.swift */,
|
||||
617AD57D60956AE03CF1A749AC9EAC63 /* CollectionDriver.swift */,
|
||||
AEAE1D9FBF4BEA1642502703476A99AA /* FlowLayout */,
|
||||
AC63961471922BAD6C682A56AA2B6BE7 /* Item */,
|
||||
27E0941C66C0AE0979F23F42144634D6 /* Section */,
|
||||
B4DFD397BE9C24A51D2824708514BEF7 /* Supplement */,
|
||||
);
|
||||
name = CollectionDriver;
|
||||
path = utopia/Source/CollectionDriver;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
827B85A573C7BACC6DE48A67B0902E37 /* DeepDiff */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1709A2FF999AEA557856138B4BEE53A4 /* iOS */,
|
||||
298C0019EBCB51F394F9FBF6A237AF84 /* Shared */,
|
||||
);
|
||||
name = DeepDiff;
|
||||
path = DeepDiff;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8C27DC0C74C30233B185E75F27845D0A /* Support Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -463,15 +378,6 @@
|
||||
path = "Example/Pods/Target Support Files/utopia";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
91132A607D073009ACB11BFAA53E075A /* Utilities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9F9BB03A7F405472718898E3CF28F4BF /* CodableUtilities.swift */,
|
||||
);
|
||||
name = Utilities;
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
93DB251A38E863705E10AF7CBB6CF7D7 /* Haptic */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -491,46 +397,15 @@
|
||||
path = utopia/Source/Types;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AC63961471922BAD6C682A56AA2B6BE7 /* Item */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7821B811634043201CB58B8B98018ED5 /* CollectionItem.swift */,
|
||||
5D8ADED2A9CC7AC64300CA0741531748 /* CollectionItemProtocol.swift */,
|
||||
3B7BDB142DD281BDC64282677DC85EAA /* SingleCellController.swift */,
|
||||
);
|
||||
name = Item;
|
||||
path = Item;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AEAE1D9FBF4BEA1642502703476A99AA /* FlowLayout */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
45C60770A293C32AF6DFEF8BD9597843 /* CollectionFlowDelegate.swift */,
|
||||
);
|
||||
name = FlowLayout;
|
||||
path = FlowLayout;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AF519AAB88449C069E4444CD0A7D84A8 /* CodingDeconding */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C664FBE14B49F100468748D16E8AA36 /* DictionaryCoding */,
|
||||
91132A607D073009ACB11BFAA53E075A /* Utilities */,
|
||||
9F9BB03A7F405472718898E3CF28F4BF /* CodableUtilities.swift */,
|
||||
);
|
||||
name = CodingDeconding;
|
||||
path = utopia/Source/CodingDeconding;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B4DFD397BE9C24A51D2824708514BEF7 /* Supplement */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BC9BCB36774C899E2CFF014515E184DF /* CollectionSupplement.swift */,
|
||||
3962543B622CFDF60B27C1E57A8EB2DC /* CollectionSupplementProtocol.swift */,
|
||||
);
|
||||
name = Supplement;
|
||||
path = Supplement;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BAA834FD327249945A0B7BD0F2560561 /* UIKit */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -541,6 +416,9 @@
|
||||
8C4EEBAEB1E1FDB6EDA95CFF0B7ECFA7 /* StatusBarAppearance.swift */,
|
||||
5C127A0077D1A43E0B1AF57D5C3E0EC4 /* Toggler.swift */,
|
||||
51A7A173B2BB4912B56024A0AB99C624 /* UISearchUtilities.swift */,
|
||||
391DB7D2215317760083B8C3 /* Gestures */,
|
||||
391DB7D1215317290083B8C3 /* Animations */,
|
||||
391DB7CC2153164E0083B8C3 /* CollectionLayout */,
|
||||
CD49264C135E357003C30ACD16A46BC9 /* Extensions */,
|
||||
E9DB469A97791CE54334A504CC837D76 /* Keyboard */,
|
||||
04906C8071F52A7C8413F1BDB4AC39D6 /* Views */,
|
||||
@@ -562,6 +440,7 @@
|
||||
children = (
|
||||
73F563779A49E6F8BABD9F7F096FAE38 /* AlphaTransition.swift */,
|
||||
9F7B5BD275A40EA5A7887E4CDB8A5084 /* ScaleModalTransitioning.swift */,
|
||||
391DB7CF215316F50083B8C3 /* InteractiveDismiss.swift */,
|
||||
6C9C018F9DF21B16BCBA1BD49027615B /* TransitioningDelegate.swift */,
|
||||
F79E5A847C4EE299FFCC34B5C4BB4354 /* UIView+transitions.swift */,
|
||||
);
|
||||
@@ -590,7 +469,6 @@
|
||||
85C649278AE9FEC1B162C60ABAAE5123 /* Time.swift */,
|
||||
FE5615B6D865408D6F06B2F71F6FA57D /* TimingFunction.swift */,
|
||||
002CE560EE32DF3EA2939698D55CA6DC /* UIColor+ext.swift */,
|
||||
827B85A573C7BACC6DE48A67B0902E37 /* DeepDiff */,
|
||||
);
|
||||
name = Utilities;
|
||||
path = utopia/Source/Utilities;
|
||||
@@ -617,7 +495,6 @@
|
||||
8BE5E5B9BC9342D12093473CC3AAD147 /* UIViewController+ext.swift */,
|
||||
EF4F6D046DADE0DD3A4E045F76D1CE16 /* UIWindow+ext.swift */,
|
||||
);
|
||||
name = Extensions;
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -654,7 +531,6 @@
|
||||
80C3D37E1C6C9537466FBA3F006E1A2D /* InputVisibilityController.swift */,
|
||||
97F35D35AEE8986797BCB7527E30A0FA /* TouchRecognizer.swift */,
|
||||
);
|
||||
name = InputVisibilityController;
|
||||
path = InputVisibilityController;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -664,21 +540,9 @@
|
||||
4D2E2CE51EEFE3790C4B1336397E3530 /* KeyboardObserver.swift */,
|
||||
E3BA05F8DF75E9B7D6DF71470F844590 /* InputVisibilityController */,
|
||||
);
|
||||
name = Keyboard;
|
||||
path = Keyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EBBAD62BE8FC301DB475A5815674E8B8 /* Algorithms */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0E215442310E056A1978EA9D5760C1D4 /* DiffAware.swift */,
|
||||
E6EB35A67E9198A6995778F2F1A9FBFD /* Heckel.swift */,
|
||||
90CCD71777EAB46247B48FA67E0B3132 /* WagnerFischer.swift */,
|
||||
);
|
||||
name = Algorithms;
|
||||
path = Algorithms;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
@@ -744,6 +608,14 @@
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0930;
|
||||
LastUpgradeCheck = 0930;
|
||||
TargetAttributes = {
|
||||
02B55224E15D80EA5F1C45D2650F46E3 = {
|
||||
LastSwiftMigration = 1000;
|
||||
};
|
||||
55FF8012DB818DE68BDEF0BC12B62C22 = {
|
||||
LastSwiftMigration = 1000;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
@@ -774,42 +646,23 @@
|
||||
FBA69FEF5BF5077D1A63854B7286CC57 /* AsyncOperation.swift in Sources */,
|
||||
DC9BC229563C9D4D76C4ADD9962E181A /* BarButtons.swift in Sources */,
|
||||
D7D7B6285FF6691B27A7D73FF196468A /* Bundle.swift in Sources */,
|
||||
F1C2819748E2528F3DC497BD3B369EB2 /* Change.swift in Sources */,
|
||||
391DB7D6215317A20083B8C3 /* TouchInteraction.swift in Sources */,
|
||||
728E7F3E40B77C6AE1F2EA03435547F9 /* CodableUtilities.swift in Sources */,
|
||||
F0A66F0F3A522BECA0BCFE8EAB863330 /* CollectionDelegate.swift in Sources */,
|
||||
ECB21B60D35987F9AFFEC942F656D70B /* CollectionDriver.swift in Sources */,
|
||||
75C03B94D42A4B2C7871A6607BB8E2D6 /* CollectionFlowDelegate.swift in Sources */,
|
||||
5788D3A4E388EEC282525FA728EFFD4B /* CollectionItem.swift in Sources */,
|
||||
6043ADCB4A6A8BF4E704C73292A5F4FE /* CollectionItemProtocol.swift in Sources */,
|
||||
D968A8500BD1AC4EC3FD58F23FCD6D43 /* Collections+ext.swift in Sources */,
|
||||
B4634EB2195A74E5705E2EB5153D3961 /* Collections+Extensions.swift in Sources */,
|
||||
A2591F809F08E89B6AD7B6DA7561154D /* CollectionSection.swift in Sources */,
|
||||
016CC3A1C1D7DDF9EDC5C1162D649B11 /* CollectionSupplement.swift in Sources */,
|
||||
A5EC34CCDF7A16CDB83E879951608D29 /* CollectionSupplementProtocol.swift in Sources */,
|
||||
C980A8B4B7D46F918161A80D523F5B9A /* Comparable+ext.swift in Sources */,
|
||||
4F58D683476B2C93625C121FAC487769 /* CoreAnimation+ext.swift in Sources */,
|
||||
93C8DEBCE66F72F7E1F2C56C3C48F212 /* CoreGraphics+ext.swift in Sources */,
|
||||
CA834F710C7B8DE7E78756369DB8B082 /* DateUtilities.swift in Sources */,
|
||||
999AFF8B082890847D8ED1EDF59F8BEE /* DeepDiff.swift in Sources */,
|
||||
737F4D68613986033F0FE3870FFD20D3 /* DictionaryCodingKey.swift in Sources */,
|
||||
CA40B44897ADD77342DE139BB3D226F0 /* DictionaryDecoder.swift in Sources */,
|
||||
57436A3B37C2DDBE9953C104FB706A81 /* DictionaryEncoder.swift in Sources */,
|
||||
46F572A824B37693AF514E85E0549C58 /* DictionaryErrors.swift in Sources */,
|
||||
76E15EE6FF172907EC6293F7C471A7ED /* DiffAware.swift in Sources */,
|
||||
826398D12955504DC62BF1167ED6D605 /* Display.swift in Sources */,
|
||||
FC73128B984C0755DA32C1E1A15A5DBB /* GradientView.swift in Sources */,
|
||||
4837A82C9004D6D819B45977200EEF92 /* Haptic.swift in Sources */,
|
||||
C1EA2BF250F881A8CE274F56698CA5ED /* Hapticable.swift in Sources */,
|
||||
2ADB6BCE9B619A9A4B87388877977E37 /* Heckel.swift in Sources */,
|
||||
9B702BEA0A412AE61A2A100047A2682A /* ImageSettable.swift in Sources */,
|
||||
A956D28489AB7AB9B6DCF30C903F3E73 /* IndexPathConverter.swift in Sources */,
|
||||
D57151A2D17D6AA1A791C365350D6637 /* IndexSetConverter.swift in Sources */,
|
||||
3C593180EE447D73A5ADBA3AE8909897 /* InputVisibilityController.swift in Sources */,
|
||||
30522776ACB49F80FB650434A08771A2 /* KeyboardObserver.swift in Sources */,
|
||||
50202062FF2FEFA5A435C025B28E6F2E /* LayoutGuides.swift in Sources */,
|
||||
32B0440AA07DD473ABD2D61DF2F3FCD8 /* Literals.swift in Sources */,
|
||||
35BB5E12A394D82FF0F8E675F3000FDB /* Math.swift in Sources */,
|
||||
F8907750D7B78472C381EA587CA1DDE8 /* MoveReducer.swift in Sources */,
|
||||
E0E0FDD5E0C600B0AC5D7E4EE318AC2B /* None.swift in Sources */,
|
||||
17C8EC8E91B5BB1EC60297B16F8B0E24 /* NSAttributedStrings+ext.swift in Sources */,
|
||||
5179A999CF3F2B5C221D379A978334B1 /* Optional+ext.swift in Sources */,
|
||||
@@ -818,11 +671,12 @@
|
||||
9E1484918CFB62F8A4DBAFD2604BDBBC /* Require.swift in Sources */,
|
||||
D5136D58B70ED60743594DDA623340A6 /* Result.swift in Sources */,
|
||||
F40A972F66F6674CDEA0187860E8785B /* ScaleModalTransitioning.swift in Sources */,
|
||||
391DB7CE215316AB0083B8C3 /* HorizontalAlignmentLayout.swift in Sources */,
|
||||
854C49070D0FE89878C8E5D0124CD65E /* ScrollDismission.swift in Sources */,
|
||||
391DB7D4215317990083B8C3 /* GestureRecognizerDelegate.swift in Sources */,
|
||||
5533FA61D479D4E52F22B6D7E5634127 /* Signal.swift in Sources */,
|
||||
35F36F6F8769CDC19189C6C44BECE03E /* SignalSubscription.swift in Sources */,
|
||||
25625A79AD94F64B1C05606994AEC409 /* SimpleError.swift in Sources */,
|
||||
77E60E92F55D44D77B524CE6712BB6FA /* SingleCellController.swift in Sources */,
|
||||
29396B3ADC9D79BC235C37FC87898C0E /* Snapshot.swift in Sources */,
|
||||
40077E1D3AAA704AC97E1AEDA22CF464 /* StatusBarAppearance.swift in Sources */,
|
||||
A848BE440EAD8450C7B15BE5DB31BAE7 /* String+ext.swift in Sources */,
|
||||
@@ -840,8 +694,8 @@
|
||||
2F281C3DB35F09EF83172BA7D2A2955E /* UIBarButtonItem+Signals.swift in Sources */,
|
||||
26DE0E6B75AB5050F5772B922A2AD509 /* UIButton+ext.swift in Sources */,
|
||||
9052FD4E4A2BF4D796D2521EE47B2069 /* UIButton+layout.swift in Sources */,
|
||||
0469A2397AB66C46BE90652653352AB2 /* UICollectionView+DeepDiff.swift in Sources */,
|
||||
0BE22BEB5AE38CEC94E0A184C8946969 /* UICollectionView+Reusable.swift in Sources */,
|
||||
391DB7D0215316F50083B8C3 /* InteractiveDismiss.swift in Sources */,
|
||||
CA706B65DFD60F03EDC1773828AC4306 /* UIColor+ext.swift in Sources */,
|
||||
696FBE84EEB2A8F325E8D551903BF524 /* UIControl+Signals.swift in Sources */,
|
||||
02807C6CD1D68F6A8F1D5F5EDBC075F3 /* UIGestureRecognizer+ext.swift in Sources */,
|
||||
@@ -851,7 +705,6 @@
|
||||
BD69427FEADE03F3A2931FDE6A90B28C /* UIScrollView+ext.swift in Sources */,
|
||||
56AFDBABD2F98A47878F3C6D24D2C18D /* UISearchUtilities.swift in Sources */,
|
||||
8CBEFDF21CBA10F3358929B79D9A29B5 /* UIStackView+ext.swift in Sources */,
|
||||
5F10FDD4882A344EBC6F15A2A7824F21 /* UITableView+DeepDiff.swift in Sources */,
|
||||
A4D0DE6907F75000EFA2D909D97426DA /* UITableView+Reusable.swift in Sources */,
|
||||
34A0B3185049A34C0C8753721B994638 /* UITextField+ext.swift in Sources */,
|
||||
A064E2905C6E6EDBB8A956E45B6E2847 /* UIView+ext.swift in Sources */,
|
||||
@@ -861,7 +714,6 @@
|
||||
2B4E363F17E50EDA2C69E578CAAE43BF /* utopia-dummy.m in Sources */,
|
||||
EAF44B11790232341E89C0702F35AE13 /* ValidatableView.swift in Sources */,
|
||||
54C77D7CC63D94A7843FA06931A72124 /* Validation.swift in Sources */,
|
||||
34121FA0A44E5638EF672F112FA7EBCD /* WagnerFischer.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -869,6 +721,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3948F8B92193A3000043FD2E /* PinnedHeaderFlowLayout.swift in Sources */,
|
||||
5B190B9D09C4A639DA32D00681E1B4A7 /* Pods-utopia_Example-dummy.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -890,6 +743,7 @@
|
||||
baseConfigurationReference = F61C18A1761D447D4249ABA4E14D11AE /* Pods-utopia_Example.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
@@ -914,6 +768,7 @@
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -947,7 +802,7 @@
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
@@ -1101,7 +956,7 @@
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -1114,6 +969,7 @@
|
||||
baseConfigurationReference = FAFC54380DC5D0CA56A4551D0E7806AE /* Pods-utopia_Example.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
@@ -1139,6 +995,7 @@
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
|
||||
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; };
|
||||
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
|
||||
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; };
|
||||
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; };
|
||||
CEAFA6DEE8FD46FA928FC6B9 /* Pods_utopia_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D96C1039201BCADF7E730F6 /* Pods_utopia_Example.framework */; };
|
||||
@@ -18,15 +16,11 @@
|
||||
/* Begin PBXFileReference section */
|
||||
0D96C1039201BCADF7E730F6 /* Pods_utopia_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_utopia_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
12909272F90F0829F3E16C48 /* utopia.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = utopia.podspec; path = ../utopia.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
|
||||
3D6E6CE2CA9E2A4ED29C9C80 /* Pods-utopia_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-utopia_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-utopia_Tests/Pods-utopia_Tests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
607FACD01AFB9204008FA782 /* utopia_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = utopia_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
607FACD71AFB9204008FA782 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
|
||||
607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
|
||||
6F466667E3C01068D5F3BABF /* Pods-utopia_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-utopia_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-utopia_Tests/Pods-utopia_Tests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
AD58C60D33BF3DE957B80316 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
|
||||
B099FDB815DE6B171E39CF89 /* Pods_utopia_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_utopia_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B60E758ED76098E52DACF178 /* Pods-utopia_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-utopia_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-utopia_Example/Pods-utopia_Example.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
@@ -78,10 +72,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
607FACD51AFB9204008FA782 /* AppDelegate.swift */,
|
||||
607FACD71AFB9204008FA782 /* ViewController.swift */,
|
||||
607FACD91AFB9204008FA782 /* Main.storyboard */,
|
||||
607FACDC1AFB9204008FA782 /* Images.xcassets */,
|
||||
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */,
|
||||
607FACD31AFB9204008FA782 /* Supporting Files */,
|
||||
);
|
||||
name = "Example for utopia";
|
||||
@@ -91,6 +81,8 @@
|
||||
607FACD31AFB9204008FA782 /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
607FACDC1AFB9204008FA782 /* Images.xcassets */,
|
||||
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */,
|
||||
607FACD41AFB9204008FA782 /* Info.plist */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
@@ -111,8 +103,6 @@
|
||||
children = (
|
||||
B60E758ED76098E52DACF178 /* Pods-utopia_Example.debug.xcconfig */,
|
||||
DFFC9C07CDE48E9551AEAD4E /* Pods-utopia_Example.release.xcconfig */,
|
||||
6F466667E3C01068D5F3BABF /* Pods-utopia_Tests.debug.xcconfig */,
|
||||
3D6E6CE2CA9E2A4ED29C9C80 /* Pods-utopia_Tests.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
@@ -146,12 +136,12 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0830;
|
||||
LastUpgradeCheck = 0830;
|
||||
LastUpgradeCheck = 1000;
|
||||
ORGANIZATIONNAME = CocoaPods;
|
||||
TargetAttributes = {
|
||||
607FACCF1AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
LastSwiftMigration = 0900;
|
||||
LastSwiftMigration = 1000;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -178,7 +168,6 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */,
|
||||
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */,
|
||||
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */,
|
||||
);
|
||||
@@ -230,7 +219,6 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */,
|
||||
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -238,14 +226,6 @@
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
607FACD91AFB9204008FA782 /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
607FACDA1AFB9204008FA782 /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
@@ -269,12 +249,14 @@
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
@@ -322,12 +304,14 @@
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
@@ -361,13 +345,13 @@
|
||||
baseConfigurationReference = B60E758ED76098E52DACF178 /* Pods-utopia_Example.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = utopia/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MODULE_NAME = ExampleApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 4.2;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -376,13 +360,13 @@
|
||||
baseConfigurationReference = DFFC9C07CDE48E9551AEAD4E /* Pods-utopia_Example.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = utopia/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MODULE_NAME = ExampleApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 4.2;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0900"
|
||||
LastUpgradeVersion = "1000"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -40,7 +40,6 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
@@ -70,7 +69,6 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// utopia
|
||||
//
|
||||
// Created by Dmitriy Kalachev on 04/09/2018.
|
||||
// Copyright (c) 2018 Dmitriy Kalachev. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@UIApplicationMain
|
||||
@@ -13,34 +5,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="ufC-wZ-h7g">
|
||||
<objects>
|
||||
<viewController id="vXZ-lx-hvc" customClass="ViewController" customModule="utopia_Example" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="jyV-Pf-zRb"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="2fi-mo-0CV"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="kh9-bI-dsS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="x5A-6p-PRh" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -24,8 +24,6 @@
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// utopia
|
||||
//
|
||||
// Created by Dmitriy Kalachev on 04/09/2018.
|
||||
// Copyright (c) 2018 Dmitriy Kalachev. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ViewController: UIViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
// Do any additional setup after loading the view, typically from a nib.
|
||||
}
|
||||
|
||||
override func didReceiveMemoryWarning() {
|
||||
super.didReceiveMemoryWarning()
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-33
@@ -1,42 +1,11 @@
|
||||
#
|
||||
# Be sure to run `pod lib lint utopia.podspec' to ensure this is a
|
||||
# valid spec before submitting.
|
||||
#
|
||||
# Any lines starting with a # are optional, but their use is encouraged
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
|
||||
#
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'utopia'
|
||||
s.version = '0.1.0'
|
||||
s.summary = 'A short description of utopia.'
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
# * Think: What does it do? Why did you write it? What is the focus?
|
||||
# * Try to keep it short, snappy and to the point.
|
||||
# * Write the description between the DESC delimiters below.
|
||||
# * Finally, don't worry about the indent, CocoaPods strips it!
|
||||
|
||||
s.description = <<-DESC
|
||||
TODO: Add long description of the pod here.
|
||||
DESC
|
||||
|
||||
s.summary = 'Common utilities for Swift projects'
|
||||
s.homepage = 'https://github.com/Ramotion/utopia'
|
||||
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'Dmitriy Kalachev' => 'dima.k@ramotion.com' }
|
||||
s.author = { 'Ramotion' => 'igor.k@ramotion.com' }
|
||||
s.source = { :git => 'https://github.com/Ramotion/utopia.git', :tag => s.version.to_s }
|
||||
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
|
||||
|
||||
s.ios.deployment_target = '10.0'
|
||||
|
||||
s.source_files = 'utopia/Source/**/*'
|
||||
|
||||
# s.resource_bundles = {
|
||||
# 'utopia' => ['utopia/Assets/*.png']
|
||||
# }
|
||||
|
||||
# s.public_header_files = 'Pod/Source/**/*.h'
|
||||
# s.frameworks = 'UIKit', 'MapKit'
|
||||
# s.dependency 'AFNetworking', '~> 2.3'
|
||||
end
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is largely a copy of code from Swift.org open source project's
|
||||
// files JSONEncoder.swift and Codeable.swift.
|
||||
//
|
||||
// Unfortunately those files do not expose the internal _JSONEncoder and
|
||||
// _JSONDecoder classes, which are in fact dictionary encoder/decoders and
|
||||
// precisely what we want...
|
||||
//
|
||||
// The original code is copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See https://swift.org/LICENSE.txt for license information
|
||||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||||
//
|
||||
// Modifications and additional code here is copyright (c) 2018 Sam Deane, and
|
||||
// is licensed under the same terms.
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
|
||||
internal struct DictionaryCodingKey : CodingKey {
|
||||
public var stringValue: String
|
||||
public var intValue: Int?
|
||||
|
||||
public init?(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
self.intValue = nil
|
||||
}
|
||||
|
||||
public init?(intValue: Int) {
|
||||
self.stringValue = "\(intValue)"
|
||||
self.intValue = intValue
|
||||
}
|
||||
|
||||
public init(stringValue: String, intValue: Int?) {
|
||||
self.stringValue = stringValue
|
||||
self.intValue = intValue
|
||||
}
|
||||
|
||||
internal init(index: Int) {
|
||||
self.stringValue = "Index \(index)"
|
||||
self.intValue = index
|
||||
}
|
||||
|
||||
internal static let `super` = DictionaryCodingKey(stringValue: "super")!
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,915 +0,0 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is largely a copy of code from Swift.org open source project's
|
||||
// files JSONEncoder.swift and Codeable.swift.
|
||||
//
|
||||
// Unfortunately those files do not expose the internal _JSONEncoder and
|
||||
// _JSONDecoder classes, which are in fact dictionary encoder/decoders and
|
||||
// precisely what we want...
|
||||
//
|
||||
// The original code is copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See https://swift.org/LICENSE.txt for license information
|
||||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||||
//
|
||||
// Modifications and additional code here is copyright (c) 2018 Sam Deane, and
|
||||
// is licensed under the same terms.
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Dictionary Encoder
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// `DictionaryEncoder` facilitates the encoding of `Encodable` values into Dictionary.
|
||||
open class DictionaryEncoder {
|
||||
// MARK: Options
|
||||
|
||||
/// The strategy to use for encoding `Date` values.
|
||||
public enum DateEncodingStrategy {
|
||||
/// Defer to `Date` for choosing an encoding. This is the default strategy.
|
||||
case deferredToDate
|
||||
|
||||
/// Encode the `Date` as a UNIX timestamp (as a Dictionary number).
|
||||
case secondsSince1970
|
||||
|
||||
/// Encode the `Date` as UNIX millisecond timestamp (as a Dictionary number).
|
||||
case millisecondsSince1970
|
||||
|
||||
/// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
|
||||
@available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
|
||||
case iso8601
|
||||
|
||||
/// Encode the `Date` as a string formatted by the given formatter.
|
||||
case formatted(DateFormatter)
|
||||
|
||||
/// Encode the `Date` as a custom value encoded by the given closure.
|
||||
///
|
||||
/// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
|
||||
case custom((Date, Encoder) throws -> Void)
|
||||
}
|
||||
|
||||
/// The strategy to use for encoding `Data` values.
|
||||
public enum DataEncodingStrategy {
|
||||
/// Defer to `Data` for choosing an encoding.
|
||||
case deferredToData
|
||||
|
||||
/// Encoded the `Data` as a Base64-encoded string. This is the default strategy.
|
||||
case base64
|
||||
|
||||
/// Encode the `Data` as a custom value encoded by the given closure.
|
||||
///
|
||||
/// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
|
||||
case custom((Data, Encoder) throws -> Void)
|
||||
}
|
||||
|
||||
/// The strategy to use for non-Dictionary-conforming floating-point values (IEEE 754 infinity and NaN).
|
||||
public enum NonConformingFloatEncodingStrategy {
|
||||
/// Throw upon encountering non-conforming values. This is the default strategy.
|
||||
case `throw`
|
||||
|
||||
/// Encode the values using the given representation strings.
|
||||
case convertToString(positiveInfinity: String, negativeInfinity: String, nan: String)
|
||||
}
|
||||
|
||||
/// The strategy to use for automatically changing the value of keys before encoding.
|
||||
public enum KeyEncodingStrategy {
|
||||
/// Use the keys specified by each type. This is the default strategy.
|
||||
case useDefaultKeys
|
||||
|
||||
/// Convert from "camelCaseKeys" to "snake_case_keys" before writing a key to Dictionary payload.
|
||||
///
|
||||
/// Capital characters are determined by testing membership in `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters` (Unicode General Categories Lu and Lt).
|
||||
/// The conversion to lower case uses `Locale.system`, also known as the ICU "root" locale. This means the result is consistent regardless of the current user's locale and language preferences.
|
||||
///
|
||||
/// Converting from camel case to snake case:
|
||||
/// 1. Splits words at the boundary of lower-case to upper-case
|
||||
/// 2. Inserts `_` between words
|
||||
/// 3. Lowercases the entire string
|
||||
/// 4. Preserves starting and ending `_`.
|
||||
///
|
||||
/// For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`.
|
||||
///
|
||||
/// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted.
|
||||
case convertToSnakeCase
|
||||
|
||||
/// Provide a custom conversion to the key in the encoded Dictionary from the keys specified by the encoded types.
|
||||
/// The full path to the current encoding position is provided for context (in case you need to locate this key within the payload). The returned key is used in place of the last component in the coding path before encoding.
|
||||
/// If the result of the conversion is a duplicate key, then only one value will be present in the result.
|
||||
case custom((_ codingPath: [CodingKey]) -> CodingKey)
|
||||
|
||||
internal static func _convertToSnakeCase(_ stringKey: String) -> String {
|
||||
guard stringKey.count > 0 else { return stringKey }
|
||||
|
||||
var words : [Range<String.Index>] = []
|
||||
// The general idea of this algorithm is to split words on transition from lower to upper case, then on transition of >1 upper case characters to lowercase
|
||||
//
|
||||
// myProperty -> my_property
|
||||
// myURLProperty -> my_url_property
|
||||
//
|
||||
// We assume, per Swift naming conventions, that the first character of the key is lowercase.
|
||||
var wordStart = stringKey.startIndex
|
||||
var searchRange = stringKey.index(after: wordStart)..<stringKey.endIndex
|
||||
|
||||
// Find next uppercase character
|
||||
while let upperCaseRange = stringKey.rangeOfCharacter(from: CharacterSet.uppercaseLetters, options: [], range: searchRange) {
|
||||
let untilUpperCase = wordStart..<upperCaseRange.lowerBound
|
||||
words.append(untilUpperCase)
|
||||
|
||||
// Find next lowercase character
|
||||
searchRange = upperCaseRange.lowerBound..<searchRange.upperBound
|
||||
guard let lowerCaseRange = stringKey.rangeOfCharacter(from: CharacterSet.lowercaseLetters, options: [], range: searchRange) else {
|
||||
// There are no more lower case letters. Just end here.
|
||||
wordStart = searchRange.lowerBound
|
||||
break
|
||||
}
|
||||
|
||||
// Is the next lowercase letter more than 1 after the uppercase? If so, we encountered a group of uppercase letters that we should treat as its own word
|
||||
let nextCharacterAfterCapital = stringKey.index(after: upperCaseRange.lowerBound)
|
||||
if lowerCaseRange.lowerBound == nextCharacterAfterCapital {
|
||||
// The next character after capital is a lower case character and therefore not a word boundary.
|
||||
// Continue searching for the next upper case for the boundary.
|
||||
wordStart = upperCaseRange.lowerBound
|
||||
} else {
|
||||
// There was a range of >1 capital letters. Turn those into a word, stopping at the capital before the lower case character.
|
||||
let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound)
|
||||
words.append(upperCaseRange.lowerBound..<beforeLowerIndex)
|
||||
|
||||
// Next word starts at the capital before the lowercase we just found
|
||||
wordStart = beforeLowerIndex
|
||||
}
|
||||
searchRange = lowerCaseRange.upperBound..<searchRange.upperBound
|
||||
}
|
||||
words.append(wordStart..<searchRange.upperBound)
|
||||
let result = words.map({ (range) in
|
||||
return stringKey[range].lowercased()
|
||||
}).joined(separator: "_")
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/// The strategy to use in encoding dates. Defaults to `.deferredToDate`.
|
||||
open var dateEncodingStrategy: DateEncodingStrategy = .deferredToDate
|
||||
|
||||
/// The strategy to use in encoding binary data. Defaults to `.base64`.
|
||||
open var dataEncodingStrategy: DataEncodingStrategy = .base64
|
||||
|
||||
/// The strategy to use in encoding non-conforming numbers. Defaults to `.throw`.
|
||||
open var nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy = .throw
|
||||
|
||||
/// The strategy to use for encoding keys. Defaults to `.useDefaultKeys`.
|
||||
open var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys
|
||||
|
||||
/// Contextual user-provided information for use during encoding.
|
||||
open var userInfo: [CodingUserInfoKey : Any] = [:]
|
||||
|
||||
/// Options set on the top-level encoder to pass down the encoding hierarchy.
|
||||
fileprivate struct _Options {
|
||||
let dateEncodingStrategy: DateEncodingStrategy
|
||||
let dataEncodingStrategy: DataEncodingStrategy
|
||||
let nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy
|
||||
let keyEncodingStrategy: KeyEncodingStrategy
|
||||
let userInfo: [CodingUserInfoKey : Any]
|
||||
}
|
||||
|
||||
/// The options set on the top-level encoder.
|
||||
fileprivate var options: _Options {
|
||||
return _Options(dateEncodingStrategy: dateEncodingStrategy,
|
||||
dataEncodingStrategy: dataEncodingStrategy,
|
||||
nonConformingFloatEncodingStrategy: nonConformingFloatEncodingStrategy,
|
||||
keyEncodingStrategy: keyEncodingStrategy,
|
||||
userInfo: userInfo)
|
||||
}
|
||||
|
||||
// MARK: - Constructing a Dictionary Encoder
|
||||
/// Initializes `self` with default strategies.
|
||||
public init() {}
|
||||
|
||||
// MARK: - Encoding Values
|
||||
/// Encodes the given top-level value and returns its Dictionary representation.
|
||||
///
|
||||
/// - parameter value: The value to encode.
|
||||
/// - returns: A new `Data` value containing the encoded Dictionary data.
|
||||
/// - throws: `EncodingError.invalidValue` if a non-conforming floating-point value is encountered during encoding, and the encoding strategy is `.throw`.
|
||||
/// - throws: An error if any value throws an error during encoding.
|
||||
open func encode<T : Encodable>(_ value: T) throws -> NSDictionary {
|
||||
let encoder = _DictionaryEncoder(options: self.options)
|
||||
|
||||
guard let topLevel = try encoder.box_(value) else {
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) did not encode any values."))
|
||||
}
|
||||
|
||||
if topLevel is NSNull {
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as null Dictionary fragment."))
|
||||
} else if topLevel is NSNumber {
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as number Dictionary fragment."))
|
||||
} else if topLevel is NSString {
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as string Dictionary fragment."))
|
||||
}
|
||||
|
||||
return topLevel as! NSDictionary
|
||||
}
|
||||
|
||||
// MARK: - Encoding Values
|
||||
/// Encodes the given top-level value and returns its Dictionary representation.
|
||||
///
|
||||
/// - parameter value: The value to encode.
|
||||
/// - returns: A new `Data` value containing the encoded Dictionary data.
|
||||
/// - throws: `EncodingError.invalidValue` if a non-conforming floating-point value is encountered during encoding, and the encoding strategy is `.throw`.
|
||||
/// - throws: An error if any value throws an error during encoding.
|
||||
open func encode<T : Encodable>(_ value: T) throws -> [String:Any] {
|
||||
let encoder = _DictionaryEncoder(options: self.options)
|
||||
|
||||
guard let topLevel = try encoder.box_(value) else {
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) did not encode any values."))
|
||||
}
|
||||
|
||||
if topLevel is NSNull {
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as null Dictionary fragment."))
|
||||
} else if topLevel is NSNumber {
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as number Dictionary fragment."))
|
||||
} else if topLevel is NSString {
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as string Dictionary fragment."))
|
||||
}
|
||||
|
||||
return topLevel as! [String:Any]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - _DictionaryEncoder
|
||||
fileprivate class _DictionaryEncoder : Encoder {
|
||||
// MARK: Properties
|
||||
/// The encoder's storage.
|
||||
fileprivate var storage: _DictionaryEncodingStorage
|
||||
|
||||
/// Options set on the top-level encoder.
|
||||
fileprivate let options: DictionaryEncoder._Options
|
||||
|
||||
/// The path to the current point in encoding.
|
||||
public var codingPath: [CodingKey]
|
||||
|
||||
/// Contextual user-provided information for use during encoding.
|
||||
public var userInfo: [CodingUserInfoKey : Any] {
|
||||
return self.options.userInfo
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` with the given top-level encoder options.
|
||||
fileprivate init(options: DictionaryEncoder._Options, codingPath: [CodingKey] = []) {
|
||||
self.options = options
|
||||
self.storage = _DictionaryEncodingStorage()
|
||||
self.codingPath = codingPath
|
||||
}
|
||||
|
||||
/// Returns whether a new element can be encoded at this coding path.
|
||||
///
|
||||
/// `true` if an element has not yet been encoded at this coding path; `false` otherwise.
|
||||
fileprivate var canEncodeNewValue: Bool {
|
||||
// Every time a new value gets encoded, the key it's encoded for is pushed onto the coding path (even if it's a nil key from an unkeyed container).
|
||||
// At the same time, every time a container is requested, a new value gets pushed onto the storage stack.
|
||||
// If there are more values on the storage stack than on the coding path, it means the value is requesting more than one container, which violates the precondition.
|
||||
//
|
||||
// This means that anytime something that can request a new container goes onto the stack, we MUST push a key onto the coding path.
|
||||
// Things which will not request containers do not need to have the coding path extended for them (but it doesn't matter if it is, because they will not reach here).
|
||||
return self.storage.count == self.codingPath.count
|
||||
}
|
||||
|
||||
// MARK: - Encoder Methods
|
||||
public func container<Key>(keyedBy: Key.Type) -> KeyedEncodingContainer<Key> {
|
||||
// If an existing keyed container was already requested, return that one.
|
||||
let topContainer: NSMutableDictionary
|
||||
if self.canEncodeNewValue {
|
||||
// We haven't yet pushed a container at this level; do so here.
|
||||
topContainer = self.storage.pushKeyedContainer()
|
||||
} else {
|
||||
guard let container = self.storage.containers.last as? NSMutableDictionary else {
|
||||
preconditionFailure("Attempt to push new keyed encoding container when already previously encoded at this path.")
|
||||
}
|
||||
|
||||
topContainer = container
|
||||
}
|
||||
|
||||
let container = DictionaryCodingKeyedEncodingContainer<Key>(referencing: self, codingPath: self.codingPath, wrapping: topContainer)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
public func unkeyedContainer() -> UnkeyedEncodingContainer {
|
||||
// If an existing unkeyed container was already requested, return that one.
|
||||
let topContainer: NSMutableArray
|
||||
if self.canEncodeNewValue {
|
||||
// We haven't yet pushed a container at this level; do so here.
|
||||
topContainer = self.storage.pushUnkeyedContainer()
|
||||
} else {
|
||||
guard let container = self.storage.containers.last as? NSMutableArray else {
|
||||
preconditionFailure("Attempt to push new unkeyed encoding container when already previously encoded at this path.")
|
||||
}
|
||||
|
||||
topContainer = container
|
||||
}
|
||||
|
||||
return _DictionaryUnkeyedEncodingContainer(referencing: self, codingPath: self.codingPath, wrapping: topContainer)
|
||||
}
|
||||
|
||||
public func singleValueContainer() -> SingleValueEncodingContainer {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Encoding Storage and Containers
|
||||
fileprivate struct _DictionaryEncodingStorage {
|
||||
// MARK: Properties
|
||||
/// The container stack.
|
||||
/// Elements may be any one of the Dictionary types (NSNull, NSNumber, NSString, NSArray, NSDictionary).
|
||||
private(set) fileprivate var containers: [NSObject] = []
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` with no containers.
|
||||
fileprivate init() {}
|
||||
|
||||
// MARK: - Modifying the Stack
|
||||
fileprivate var count: Int {
|
||||
return self.containers.count
|
||||
}
|
||||
|
||||
fileprivate mutating func pushKeyedContainer() -> NSMutableDictionary {
|
||||
let dictionary = NSMutableDictionary()
|
||||
self.containers.append(dictionary)
|
||||
return dictionary
|
||||
}
|
||||
|
||||
fileprivate mutating func pushUnkeyedContainer() -> NSMutableArray {
|
||||
let array = NSMutableArray()
|
||||
self.containers.append(array)
|
||||
return array
|
||||
}
|
||||
|
||||
fileprivate mutating func push(container: NSObject) {
|
||||
self.containers.append(container)
|
||||
}
|
||||
|
||||
fileprivate mutating func popContainer() -> NSObject {
|
||||
precondition(self.containers.count > 0, "Empty container stack.")
|
||||
return self.containers.popLast()!
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Encoding Containers
|
||||
fileprivate struct DictionaryCodingKeyedEncodingContainer<K : CodingKey> : KeyedEncodingContainerProtocol {
|
||||
typealias Key = K
|
||||
|
||||
// MARK: Properties
|
||||
/// A reference to the encoder we're writing to.
|
||||
private let encoder: _DictionaryEncoder
|
||||
|
||||
/// A reference to the container we're writing to.
|
||||
private let container: NSMutableDictionary
|
||||
|
||||
/// The path of coding keys taken to get to this point in encoding.
|
||||
private(set) public var codingPath: [CodingKey]
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` with the given references.
|
||||
fileprivate init(referencing encoder: _DictionaryEncoder, codingPath: [CodingKey], wrapping container: NSMutableDictionary) {
|
||||
self.encoder = encoder
|
||||
self.codingPath = codingPath
|
||||
self.container = container
|
||||
}
|
||||
|
||||
// MARK: - Coding Path Operations
|
||||
private func _converted(_ key: CodingKey) -> CodingKey {
|
||||
switch encoder.options.keyEncodingStrategy {
|
||||
case .useDefaultKeys:
|
||||
return key
|
||||
case .convertToSnakeCase:
|
||||
let newKeyString = DictionaryEncoder.KeyEncodingStrategy._convertToSnakeCase(key.stringValue)
|
||||
return DictionaryCodingKey(stringValue: newKeyString, intValue: key.intValue)
|
||||
case .custom(let converter):
|
||||
return converter(codingPath + [key])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - KeyedEncodingContainerProtocol Methods
|
||||
public mutating func encodeNil(forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = NSNull()
|
||||
}
|
||||
public mutating func encode(_ value: Bool, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: Int, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: Int8, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: Int16, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: Int32, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: Int64, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: UInt, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: UInt8, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: UInt16, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: UInt32, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: UInt64, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: String, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
|
||||
public mutating func encode(_ value: Float, forKey key: Key) throws {
|
||||
// Since the float may be invalid and throw, the coding path needs to contain this key.
|
||||
self.encoder.codingPath.append(key)
|
||||
defer { self.encoder.codingPath.removeLast() }
|
||||
self.container[_converted(key).stringValue] = try self.encoder.box(value)
|
||||
}
|
||||
|
||||
public mutating func encode(_ value: Double, forKey key: Key) throws {
|
||||
// Since the double may be invalid and throw, the coding path needs to contain this key.
|
||||
self.encoder.codingPath.append(key)
|
||||
defer { self.encoder.codingPath.removeLast() }
|
||||
self.container[_converted(key).stringValue] = try self.encoder.box(value)
|
||||
}
|
||||
|
||||
public mutating func encode<T : Encodable>(_ value: T, forKey key: Key) throws {
|
||||
self.encoder.codingPath.append(key)
|
||||
defer { self.encoder.codingPath.removeLast() }
|
||||
self.container[_converted(key).stringValue] = try self.encoder.box(value)
|
||||
}
|
||||
|
||||
public mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> {
|
||||
let dictionary = NSMutableDictionary()
|
||||
self.container[_converted(key).stringValue] = dictionary
|
||||
|
||||
self.codingPath.append(key)
|
||||
defer { self.codingPath.removeLast() }
|
||||
|
||||
let container = DictionaryCodingKeyedEncodingContainer<NestedKey>(referencing: self.encoder, codingPath: self.codingPath, wrapping: dictionary)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
|
||||
let array = NSMutableArray()
|
||||
self.container[_converted(key).stringValue] = array
|
||||
|
||||
self.codingPath.append(key)
|
||||
defer { self.codingPath.removeLast() }
|
||||
return _DictionaryUnkeyedEncodingContainer(referencing: self.encoder, codingPath: self.codingPath, wrapping: array)
|
||||
}
|
||||
|
||||
public mutating func superEncoder() -> Encoder {
|
||||
return _DictionaryReferencingEncoder(referencing: self.encoder, key: DictionaryCodingKey.super, convertedKey: _converted(DictionaryCodingKey.super), wrapping: self.container)
|
||||
}
|
||||
|
||||
public mutating func superEncoder(forKey key: Key) -> Encoder {
|
||||
return _DictionaryReferencingEncoder(referencing: self.encoder, key: key, convertedKey: _converted(key), wrapping: self.container)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct _DictionaryUnkeyedEncodingContainer : UnkeyedEncodingContainer {
|
||||
// MARK: Properties
|
||||
/// A reference to the encoder we're writing to.
|
||||
private let encoder: _DictionaryEncoder
|
||||
|
||||
/// A reference to the container we're writing to.
|
||||
private let container: NSMutableArray
|
||||
|
||||
/// The path of coding keys taken to get to this point in encoding.
|
||||
private(set) public var codingPath: [CodingKey]
|
||||
|
||||
/// The number of elements encoded into the container.
|
||||
public var count: Int {
|
||||
return self.container.count
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` with the given references.
|
||||
fileprivate init(referencing encoder: _DictionaryEncoder, codingPath: [CodingKey], wrapping container: NSMutableArray) {
|
||||
self.encoder = encoder
|
||||
self.codingPath = codingPath
|
||||
self.container = container
|
||||
}
|
||||
|
||||
// MARK: - UnkeyedEncodingContainer Methods
|
||||
public mutating func encodeNil() throws { self.container.add(NSNull()) }
|
||||
public mutating func encode(_ value: Bool) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int8) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int16) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int32) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int64) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt8) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt16) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt32) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt64) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: String) throws { self.container.add(self.encoder.box(value)) }
|
||||
|
||||
public mutating func encode(_ value: Float) throws {
|
||||
// Since the float may be invalid and throw, the coding path needs to contain this key.
|
||||
self.encoder.codingPath.append(DictionaryCodingKey(index: self.count))
|
||||
defer { self.encoder.codingPath.removeLast() }
|
||||
self.container.add(try self.encoder.box(value))
|
||||
}
|
||||
|
||||
public mutating func encode(_ value: Double) throws {
|
||||
// Since the double may be invalid and throw, the coding path needs to contain this key.
|
||||
self.encoder.codingPath.append(DictionaryCodingKey(index: self.count))
|
||||
defer { self.encoder.codingPath.removeLast() }
|
||||
self.container.add(try self.encoder.box(value))
|
||||
}
|
||||
|
||||
public mutating func encode<T : Encodable>(_ value: T) throws {
|
||||
self.encoder.codingPath.append(DictionaryCodingKey(index: self.count))
|
||||
defer { self.encoder.codingPath.removeLast() }
|
||||
self.container.add(try self.encoder.box(value))
|
||||
}
|
||||
|
||||
public mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> {
|
||||
self.codingPath.append(DictionaryCodingKey(index: self.count))
|
||||
defer { self.codingPath.removeLast() }
|
||||
|
||||
let dictionary = NSMutableDictionary()
|
||||
self.container.add(dictionary)
|
||||
|
||||
let container = DictionaryCodingKeyedEncodingContainer<NestedKey>(referencing: self.encoder, codingPath: self.codingPath, wrapping: dictionary)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
public mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
|
||||
self.codingPath.append(DictionaryCodingKey(index: self.count))
|
||||
defer { self.codingPath.removeLast() }
|
||||
|
||||
let array = NSMutableArray()
|
||||
self.container.add(array)
|
||||
return _DictionaryUnkeyedEncodingContainer(referencing: self.encoder, codingPath: self.codingPath, wrapping: array)
|
||||
}
|
||||
|
||||
public mutating func superEncoder() -> Encoder {
|
||||
return _DictionaryReferencingEncoder(referencing: self.encoder, at: self.container.count, wrapping: self.container)
|
||||
}
|
||||
}
|
||||
|
||||
extension _DictionaryEncoder : SingleValueEncodingContainer {
|
||||
// MARK: - SingleValueEncodingContainer Methods
|
||||
fileprivate func assertCanEncodeNewValue() {
|
||||
precondition(self.canEncodeNewValue, "Attempt to encode value through single value container when previously value already encoded.")
|
||||
}
|
||||
|
||||
public func encodeNil() throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: NSNull())
|
||||
}
|
||||
|
||||
public func encode(_ value: Bool) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int8) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int16) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int32) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int64) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt8) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt16) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt32) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt64) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: String) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Float) throws {
|
||||
assertCanEncodeNewValue()
|
||||
try self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Double) throws {
|
||||
assertCanEncodeNewValue()
|
||||
try self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode<T : Encodable>(_ value: T) throws {
|
||||
assertCanEncodeNewValue()
|
||||
try self.storage.push(container: self.box(value))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Concrete Value Representations
|
||||
extension _DictionaryEncoder {
|
||||
/// Returns the given value boxed in a container appropriate for pushing onto the container stack.
|
||||
fileprivate func box(_ value: Bool) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int8) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int16) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int32) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int64) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt8) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt16) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt32) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt64) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: String) -> NSObject { return NSString(string: value) }
|
||||
|
||||
fileprivate func box(_ float: Float) throws -> NSObject {
|
||||
guard !float.isInfinite && !float.isNaN else {
|
||||
guard case let .convertToString(positiveInfinity: posInfString,
|
||||
negativeInfinity: negInfString,
|
||||
nan: nanString) = self.options.nonConformingFloatEncodingStrategy else {
|
||||
throw EncodingError._invalidFloatingPointValue(float, at: codingPath)
|
||||
}
|
||||
|
||||
if float == Float.infinity {
|
||||
return NSString(string: posInfString)
|
||||
} else if float == -Float.infinity {
|
||||
return NSString(string: negInfString)
|
||||
} else {
|
||||
return NSString(string: nanString)
|
||||
}
|
||||
}
|
||||
|
||||
return NSNumber(value: float)
|
||||
}
|
||||
|
||||
fileprivate func box(_ double: Double) throws -> NSObject {
|
||||
guard !double.isInfinite && !double.isNaN else {
|
||||
guard case let .convertToString(positiveInfinity: posInfString,
|
||||
negativeInfinity: negInfString,
|
||||
nan: nanString) = self.options.nonConformingFloatEncodingStrategy else {
|
||||
throw EncodingError._invalidFloatingPointValue(double, at: codingPath)
|
||||
}
|
||||
|
||||
if double == Double.infinity {
|
||||
return NSString(string: posInfString)
|
||||
} else if double == -Double.infinity {
|
||||
return NSString(string: negInfString)
|
||||
} else {
|
||||
return NSString(string: nanString)
|
||||
}
|
||||
}
|
||||
|
||||
return NSNumber(value: double)
|
||||
}
|
||||
|
||||
fileprivate func box(_ date: Date) throws -> NSObject {
|
||||
switch self.options.dateEncodingStrategy {
|
||||
case .deferredToDate:
|
||||
// Must be called with a surrounding with(pushedKey:) call.
|
||||
// Dates encode as single-value objects; this can't both throw and push a container, so no need to catch the error.
|
||||
try date.encode(to: self)
|
||||
return self.storage.popContainer()
|
||||
|
||||
case .secondsSince1970:
|
||||
return NSNumber(value: date.timeIntervalSince1970)
|
||||
|
||||
case .millisecondsSince1970:
|
||||
return NSNumber(value: 1000.0 * date.timeIntervalSince1970)
|
||||
|
||||
case .iso8601:
|
||||
if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
|
||||
return NSString(string: _iso8601Formatter.string(from: date))
|
||||
} else {
|
||||
fatalError("ISO8601DateFormatter is unavailable on this platform.")
|
||||
}
|
||||
|
||||
case .formatted(let formatter):
|
||||
return NSString(string: formatter.string(from: date))
|
||||
|
||||
case .custom(let closure):
|
||||
let depth = self.storage.count
|
||||
do {
|
||||
try closure(date, self)
|
||||
} catch {
|
||||
// If the value pushed a container before throwing, pop it back off to restore state.
|
||||
if self.storage.count > depth {
|
||||
let _ = self.storage.popContainer()
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
guard self.storage.count > depth else {
|
||||
// The closure didn't encode anything. Return the default keyed container.
|
||||
return NSDictionary()
|
||||
}
|
||||
|
||||
// We can pop because the closure encoded something.
|
||||
return self.storage.popContainer()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func box(_ data: Data) throws -> NSObject {
|
||||
switch self.options.dataEncodingStrategy {
|
||||
case .deferredToData:
|
||||
// Must be called with a surrounding with(pushedKey:) call.
|
||||
let depth = self.storage.count
|
||||
do {
|
||||
try data.encode(to: self)
|
||||
} catch {
|
||||
// If the value pushed a container before throwing, pop it back off to restore state.
|
||||
// This shouldn't be possible for Data (which encodes as an array of bytes), but it can't hurt to catch a failure.
|
||||
if self.storage.count > depth {
|
||||
let _ = self.storage.popContainer()
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
return self.storage.popContainer()
|
||||
|
||||
case .base64:
|
||||
return NSString(string: data.base64EncodedString())
|
||||
|
||||
case .custom(let closure):
|
||||
let depth = self.storage.count
|
||||
do {
|
||||
try closure(data, self)
|
||||
} catch {
|
||||
// If the value pushed a container before throwing, pop it back off to restore state.
|
||||
if self.storage.count > depth {
|
||||
let _ = self.storage.popContainer()
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
guard self.storage.count > depth else {
|
||||
// The closure didn't encode anything. Return the default keyed container.
|
||||
return NSDictionary()
|
||||
}
|
||||
|
||||
// We can pop because the closure encoded something.
|
||||
return self.storage.popContainer()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func box<T : Encodable>(_ value: T) throws -> NSObject {
|
||||
return try self.box_(value) ?? NSDictionary()
|
||||
}
|
||||
|
||||
// This method is called "box_" instead of "box" to disambiguate it from the overloads. Because the return type here is different from all of the "box" overloads (and is more general), any "box" calls in here would call back into "box" recursively instead of calling the appropriate overload, which is not what we want.
|
||||
fileprivate func box_<T : Encodable>(_ value: T) throws -> NSObject? {
|
||||
if T.self == Date.self || T.self == NSDate.self {
|
||||
// Respect Date encoding strategy
|
||||
return try self.box((value as! Date))
|
||||
} else if T.self == Data.self || T.self == NSData.self {
|
||||
// Respect Data encoding strategy
|
||||
return try self.box((value as! Data))
|
||||
} else if T.self == URL.self || T.self == NSURL.self {
|
||||
// Encode URLs as single strings.
|
||||
return self.box((value as! URL).absoluteString)
|
||||
} else if T.self == Decimal.self || T.self == NSDecimalNumber.self {
|
||||
// DictionarySerialization can natively handle NSDecimalNumber.
|
||||
return (value as! NSDecimalNumber)
|
||||
}
|
||||
|
||||
// The value should request a container from the _DictionaryEncoder.
|
||||
let depth = self.storage.count
|
||||
do {
|
||||
try value.encode(to: self)
|
||||
} catch {
|
||||
// If the value pushed a container before throwing, pop it back off to restore state.
|
||||
if self.storage.count > depth {
|
||||
let _ = self.storage.popContainer()
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
// The top container should be a new container.
|
||||
guard self.storage.count > depth else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.storage.popContainer()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - _DictionaryReferencingEncoder
|
||||
/// _DictionaryReferencingEncoder is a special subclass of _DictionaryEncoder which has its own storage, but references the contents of a different encoder.
|
||||
/// It's used in superEncoder(), which returns a new encoder for encoding a superclass -- the lifetime of the encoder should not escape the scope it's created in, but it doesn't necessarily know when it's done being used (to write to the original container).
|
||||
fileprivate class _DictionaryReferencingEncoder : _DictionaryEncoder {
|
||||
// MARK: Reference types.
|
||||
/// The type of container we're referencing.
|
||||
private enum Reference {
|
||||
/// Referencing a specific index in an array container.
|
||||
case array(NSMutableArray, Int)
|
||||
|
||||
/// Referencing a specific key in a dictionary container.
|
||||
case dictionary(NSMutableDictionary, String)
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
/// The encoder we're referencing.
|
||||
fileprivate let encoder: _DictionaryEncoder
|
||||
|
||||
/// The container reference itself.
|
||||
private let reference: Reference
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` by referencing the given array container in the given encoder.
|
||||
fileprivate init(referencing encoder: _DictionaryEncoder, at index: Int, wrapping array: NSMutableArray) {
|
||||
self.encoder = encoder
|
||||
self.reference = .array(array, index)
|
||||
super.init(options: encoder.options, codingPath: encoder.codingPath)
|
||||
|
||||
self.codingPath.append(DictionaryCodingKey(index: index))
|
||||
}
|
||||
|
||||
/// Initializes `self` by referencing the given dictionary container in the given encoder.
|
||||
fileprivate init(referencing encoder: _DictionaryEncoder,
|
||||
key: CodingKey, convertedKey: CodingKey, wrapping dictionary: NSMutableDictionary) {
|
||||
self.encoder = encoder
|
||||
self.reference = .dictionary(dictionary, convertedKey.stringValue)
|
||||
super.init(options: encoder.options, codingPath: encoder.codingPath)
|
||||
|
||||
self.codingPath.append(key)
|
||||
}
|
||||
|
||||
// MARK: - Coding Path Operations
|
||||
fileprivate override var canEncodeNewValue: Bool {
|
||||
// With a regular encoder, the storage and coding path grow together.
|
||||
// A referencing encoder, however, inherits its parents coding path, as well as the key it was created for.
|
||||
// We have to take this into account.
|
||||
return self.storage.count == self.codingPath.count - self.encoder.codingPath.count - 1
|
||||
}
|
||||
|
||||
// MARK: - Deinitialization
|
||||
// Finalizes `self` by writing the contents of our storage to the referenced encoder's storage.
|
||||
deinit {
|
||||
let value: Any
|
||||
switch self.storage.count {
|
||||
case 0: value = NSDictionary()
|
||||
case 1: value = self.storage.popContainer()
|
||||
default: fatalError("Referencing encoder deallocated with multiple containers on stack.")
|
||||
}
|
||||
|
||||
switch self.reference {
|
||||
case .array(let array, let index):
|
||||
array.insert(value, at: index)
|
||||
|
||||
case .dictionary(let dictionary, let key):
|
||||
dictionary[NSString(string: key)] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is largely a copy of code from Swift.org open source project's
|
||||
// files JSONEncoder.swift and Codeable.swift.
|
||||
//
|
||||
// Unfortunately those files do not expose the internal _JSONEncoder and
|
||||
// _JSONDecoder classes, which are in fact dictionary encoder/decoders and
|
||||
// precisely what we want...
|
||||
//
|
||||
// The original code is copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See https://swift.org/LICENSE.txt for license information
|
||||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||||
//
|
||||
// Modifications and additional code here is copyright (c) 2018 Sam Deane, and
|
||||
// is licensed under the same terms.
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Error Utilities
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
internal extension EncodingError {
|
||||
/// Returns a `.invalidValue` error describing the given invalid floating-point value.
|
||||
///
|
||||
///
|
||||
/// - parameter value: The value that was invalid to encode.
|
||||
/// - parameter path: The path of `CodingKey`s taken to encode this value.
|
||||
/// - returns: An `EncodingError` with the appropriate path and debug description.
|
||||
internal static func _invalidFloatingPointValue<T : FloatingPoint>(_ value: T, at codingPath: [CodingKey]) -> EncodingError {
|
||||
let valueDescription: String
|
||||
if value == T.infinity {
|
||||
valueDescription = "\(T.self).infinity"
|
||||
} else if value == -T.infinity {
|
||||
valueDescription = "-\(T.self).infinity"
|
||||
} else {
|
||||
valueDescription = "\(T.self).nan"
|
||||
}
|
||||
|
||||
let debugDescription = "Unable to encode \(valueDescription) directly in Dictionary. Use DictionaryEncoder.NonConformingFloatEncodingStrategy.convertToString to specify how the value should be encoded."
|
||||
return .invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: debugDescription))
|
||||
}
|
||||
}
|
||||
|
||||
internal extension DecodingError {
|
||||
/// Returns a `.typeMismatch` error describing the expected type.
|
||||
///
|
||||
/// - parameter path: The path of `CodingKey`s taken to decode a value of this type.
|
||||
/// - parameter expectation: The type expected to be encountered.
|
||||
/// - parameter reality: The value that was encountered instead of the expected type.
|
||||
/// - returns: A `DecodingError` with the appropriate path and debug description.
|
||||
internal static func _typeMismatch(at path: [CodingKey], expectation: Any.Type, reality: Any) -> DecodingError {
|
||||
let description = "Expected to decode \(expectation) but found \(_typeDescription(of: reality)) instead."
|
||||
return .typeMismatch(expectation, Context(codingPath: path, debugDescription: description))
|
||||
}
|
||||
|
||||
/// Returns a description of the type of `value` appropriate for an error message.
|
||||
///
|
||||
/// - parameter value: The value whose type to describe.
|
||||
/// - returns: A string describing `value`.
|
||||
/// - precondition: `value` is one of the types below.
|
||||
internal static func _typeDescription(of value: Any) -> String {
|
||||
if value is NSNull {
|
||||
return "a null value"
|
||||
} else if value is NSNumber /* FIXM: If swift-corelibs-foundation isn't updated to use NSNumber, this check will be necessary: || value is Int || value is Double */ {
|
||||
return "a number"
|
||||
} else if value is String {
|
||||
return "a string/data"
|
||||
} else if value is [Any] {
|
||||
return "an array"
|
||||
} else if value is [String : Any] {
|
||||
return "a dictionary"
|
||||
} else {
|
||||
return "\(type(of: value))"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
open class CollectionDelegate: NSObject, UICollectionViewDelegate {
|
||||
|
||||
weak var scrollViewDelegate: UIScrollViewDelegate?
|
||||
|
||||
public weak var collectionView: UICollectionView? {
|
||||
didSet {
|
||||
collectionView?.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
public let collectionDriver: CollectionDriver
|
||||
|
||||
public init(driver: CollectionDriver) {
|
||||
self.collectionDriver = driver
|
||||
super.init()
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
guard !collectionDriver.isShowingSkeleton else { return }
|
||||
collectionDriver.item(at: indexPath).willDisplay(cell)
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard let cell = collectionView.cellForItem(at: indexPath) else { return }
|
||||
collectionDriver.item(at: indexPath).didSelect(cell)
|
||||
}
|
||||
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
scrollViewDelegate?.scrollViewDidScroll?(scrollView)
|
||||
|
||||
for section in 0..<collectionDriver.sections.count {
|
||||
let indexPath = IndexPath(item: 0, section: section)
|
||||
collectionDriver.supplement(ofKind: UICollectionElementKindSectionHeader, at: indexPath)?.didScroll(scrollView)
|
||||
collectionDriver.supplement(ofKind: UICollectionElementKindSectionFooter, at: indexPath)?.didScroll(scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
||||
scrollViewDelegate?.scrollViewDidZoom?(scrollView)
|
||||
}
|
||||
|
||||
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
scrollViewDelegate?.scrollViewWillBeginDragging?(scrollView)
|
||||
}
|
||||
|
||||
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
scrollViewDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
scrollViewDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
|
||||
}
|
||||
|
||||
public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
|
||||
scrollViewDelegate?.scrollViewWillBeginDecelerating?(scrollView)
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
scrollViewDelegate?.scrollViewDidEndDecelerating?(scrollView)
|
||||
}
|
||||
|
||||
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||
scrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView)
|
||||
}
|
||||
|
||||
public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||
return scrollViewDelegate?.viewForZooming?(in: scrollView)
|
||||
}
|
||||
|
||||
public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
|
||||
scrollViewDelegate?.scrollViewWillBeginZooming?(scrollView, with: view)
|
||||
}
|
||||
|
||||
public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
|
||||
scrollViewDelegate?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale)
|
||||
}
|
||||
|
||||
public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
|
||||
return scrollViewDelegate?.scrollViewShouldScrollToTop?(scrollView) ?? true
|
||||
}
|
||||
|
||||
public func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
|
||||
scrollViewDelegate?.scrollViewDidScrollToTop?(scrollView)
|
||||
}
|
||||
}
|
||||
@@ -1,348 +0,0 @@
|
||||
import UIKit
|
||||
import SkeletonView
|
||||
|
||||
public class CollectionDriver {
|
||||
public private(set) var mode = DisplayMode.normal
|
||||
private var diffMode = DiffMode.immediate
|
||||
|
||||
var willPerformBatchUpdate: ((UICollectionView) -> Void)? = nil
|
||||
var didPerformBatchUpdate: ((UICollectionView) -> Void)? = nil
|
||||
|
||||
public var collectionView: UICollectionView? {
|
||||
didSet {
|
||||
sections.attachTo(collectionView, driver: self)
|
||||
|
||||
if let collectionView = collectionView {
|
||||
collectionView.dataSource = wrapper
|
||||
collectionView.prefetchDataSource = wrapper
|
||||
collectionView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var sections: [CollectionSection] {
|
||||
didSet {
|
||||
oldValue.attachTo(nil)
|
||||
sections.attachTo(collectionView, driver: self)
|
||||
|
||||
enqueueDiff {
|
||||
let changes = diff(old: oldValue, new: self.sections)
|
||||
return .sections(changes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var emptyView: UIView
|
||||
|
||||
var isShowingSkeleton: Bool {
|
||||
guard let collectionView = collectionView else { return false }
|
||||
return !(collectionView.dataSource is CollectionDataSourceWrapper)
|
||||
}
|
||||
|
||||
private lazy var wrapper = CollectionDataSourceWrapper(collectionDriver: self)
|
||||
|
||||
// MARK: -
|
||||
|
||||
public init(sections: [CollectionSection] = [], emptyView: UIView = EmptyView.noData) {
|
||||
self.sections = sections
|
||||
self.emptyView = emptyView
|
||||
}
|
||||
|
||||
// MARK: - Modes
|
||||
|
||||
public func setLoadingMode(_ loadingMode: DisplayMode.LoadingMode) {
|
||||
self.mode = .loading(loadingMode)
|
||||
guard let collectionView = collectionView else { return }
|
||||
|
||||
switch loadingMode {
|
||||
case .refreshControl:
|
||||
if let refreshControl = collectionView.refreshControl, !refreshControl.isRefreshing {
|
||||
refreshControl.beginRefreshing()
|
||||
}
|
||||
case .skeleton:
|
||||
hideError()
|
||||
collectionView.showSkeleton(usingColor: UIColor.unselected)
|
||||
}
|
||||
}
|
||||
|
||||
public func refreshSkeleton() {
|
||||
guard let collectionView = collectionView else { return }
|
||||
collectionView.hideSkeleton()
|
||||
collectionView.alpha = 0
|
||||
delay(TimeInterval.oneFrame) {
|
||||
collectionView.showSkeleton(usingColor: UIColor.unselected)
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
collectionView.alpha = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func setNormalMode(_ closure: @escaping () -> Void = {}) {
|
||||
let oldMode = mode
|
||||
mode = .normal
|
||||
|
||||
guard let collectionView = collectionView else { return }
|
||||
|
||||
hideError()
|
||||
|
||||
switch oldMode {
|
||||
case .loading(.skeleton):
|
||||
collectionView.hideSkeleton(reloadDataAfter: false)
|
||||
|
||||
// Disable diffing to avoid crashes and stuff
|
||||
diffMode = .disabled
|
||||
|
||||
closure()
|
||||
collectionView.reloadData()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// Reload data completed so it's safe to enable diff again
|
||||
self.diffMode = .immediate
|
||||
}
|
||||
|
||||
case .loading(.refreshControl):
|
||||
collectionView.refreshControl?.endRefreshing()
|
||||
fallthrough
|
||||
|
||||
case .error, .normal:
|
||||
DispatchQueue.main.async {
|
||||
closure()
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
collectionView.backgroundView = self.isEmpty ? self.emptyView : nil
|
||||
}
|
||||
}
|
||||
|
||||
public func setErrorMode(_ error: Error) {
|
||||
let oldMode = mode
|
||||
mode = .error(error)
|
||||
|
||||
guard let collectionView = collectionView else { return }
|
||||
|
||||
switch oldMode {
|
||||
case .loading(.skeleton):
|
||||
collectionView.hideSkeleton(reloadDataAfter: false)
|
||||
|
||||
case .loading(.refreshControl):
|
||||
collectionView.refreshControl?.endRefreshing()
|
||||
|
||||
case .error, .normal:
|
||||
break
|
||||
}
|
||||
|
||||
if let errorView = collectionView.backgroundView as? ErrorView {
|
||||
errorView.error = error
|
||||
} else {
|
||||
let errorView = ErrorView()
|
||||
errorView.error = error
|
||||
collectionView.backgroundView = errorView
|
||||
}
|
||||
}
|
||||
|
||||
private func hideError() {
|
||||
collectionView?.backgroundView = nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Diff
|
||||
|
||||
public func batchUpdate(_ closure: () -> Void) {
|
||||
diffMode = .enqueue
|
||||
closure()
|
||||
|
||||
if let collectionView = collectionView {
|
||||
willPerformBatchUpdate?(collectionView)
|
||||
collectionView.performBatchUpdates({
|
||||
diffQueue.forEach { applyDiffResult($0) }
|
||||
diffQueue = []
|
||||
}, completion: { [weak self] _ in
|
||||
guard let `self` = self, let collectionView = self.collectionView else { return }
|
||||
self.didPerformBatchUpdate?(collectionView)
|
||||
})
|
||||
}
|
||||
|
||||
diffMode = .immediate
|
||||
}
|
||||
|
||||
enum DiffResult {
|
||||
case items([Change<CollectionItem>], section: Int)
|
||||
case sections([Change<CollectionSection>])
|
||||
}
|
||||
|
||||
private var diffQueue: [DiffResult] = []
|
||||
|
||||
func enqueueDiff(_ calculateDiff: @escaping () -> DiffResult) {
|
||||
guard let collectionView = collectionView else { return }
|
||||
|
||||
switch diffMode {
|
||||
case .immediate:
|
||||
willPerformBatchUpdate?(collectionView)
|
||||
collectionView.performBatchUpdates({
|
||||
applyDiffResult( calculateDiff() )
|
||||
}, completion: { [weak self] _ in
|
||||
guard let `self` = self, let collectionView = self.collectionView else { return }
|
||||
self.didPerformBatchUpdate?(collectionView)
|
||||
})
|
||||
case .enqueue:
|
||||
diffQueue.append( calculateDiff() )
|
||||
|
||||
case .disabled:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func applyDiffResult(_ result: DiffResult) {
|
||||
guard let collectionView = collectionView else { return }
|
||||
switch result {
|
||||
case let .items(changes, section: sectionIndex):
|
||||
collectionView.reload(changes: changes, section: sectionIndex, calledInsideBatch: true)
|
||||
|
||||
case let .sections(changes):
|
||||
collectionView.reloadSections(changes: changes, calledInsideBatch: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Data
|
||||
|
||||
public var isEmpty: Bool {
|
||||
return sections.reduce(0, { $0 + $1.items.count }) == 0
|
||||
}
|
||||
|
||||
public func item(at indexPath: IndexPath) -> CollectionItem {
|
||||
return sections[indexPath.section].items[indexPath.item]
|
||||
}
|
||||
|
||||
public func safeItem(at indexPath: IndexPath) -> CollectionItem? {
|
||||
guard indexPath.section < sections.endIndex,
|
||||
indexPath.item < sections[indexPath.section].items.endIndex
|
||||
else { return nil }
|
||||
return sections[indexPath.section].items[indexPath.item]
|
||||
}
|
||||
|
||||
public func supplement(ofKind: String, at indexPath: IndexPath) -> CollectionSupplement? {
|
||||
return sections[indexPath.section].supplementaryItems[ofKind]?[indexPath.item]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Mode Definition
|
||||
|
||||
extension CollectionDriver {
|
||||
public enum DisplayMode {
|
||||
case normal
|
||||
case loading(LoadingMode)
|
||||
case error(Error)
|
||||
|
||||
public enum LoadingMode {
|
||||
case refreshControl
|
||||
case skeleton
|
||||
}
|
||||
}
|
||||
|
||||
enum DiffMode {
|
||||
case immediate
|
||||
case enqueue
|
||||
case disabled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Wrapper
|
||||
|
||||
private final class CollectionDataSourceWrapper: NSObject, SkeletonCollectionViewDataSource, UICollectionViewDataSourcePrefetching {
|
||||
|
||||
unowned let collectionDriver: CollectionDriver
|
||||
|
||||
init(collectionDriver: CollectionDriver) {
|
||||
self.collectionDriver = collectionDriver
|
||||
}
|
||||
|
||||
|
||||
// MARK: Data Source
|
||||
|
||||
func numberOfSections(in collectionView: UICollectionView) -> Int {
|
||||
return collectionDriver.sections.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return collectionDriver.sections[section].items.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionDriver.item(at: indexPath).dequeueCell()
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
|
||||
if let supplement = collectionDriver.supplement(ofKind: kind, at: indexPath) {
|
||||
return supplement.dequeueView()
|
||||
} else {
|
||||
return UICollectionReusableView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Skeleton
|
||||
|
||||
func numSections(in collectionSkeletonView: UICollectionView) -> Int {
|
||||
return collectionDriver.sections.count
|
||||
}
|
||||
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, cellIdentifierForItemAt indexPath: IndexPath) -> ReusableCellIdentifier {
|
||||
return collectionDriver.sections[indexPath.section].skeletonCellIdentifier.require(hint: "Specify skeleton cell")
|
||||
}
|
||||
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
if let count = collectionDriver.sections[section].skeletonCellsCount {
|
||||
return count
|
||||
} else if let collection = collectionDriver.collectionView,
|
||||
let flowlayout = collection.collectionViewLayout as? UICollectionViewFlowLayout {
|
||||
let count = Int(ceil(collection.frame.height/flowlayout.itemSize.height))
|
||||
return count
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Prefetch
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
|
||||
guard !collectionDriver.isShowingSkeleton else { return }
|
||||
indexPaths.forEach {
|
||||
collectionDriver
|
||||
.safeItem(at: $0)? // indexPaths contains non-existing items if updating
|
||||
.prefetch()
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
|
||||
guard !collectionDriver.isShowingSkeleton else { return }
|
||||
indexPaths.forEach {
|
||||
collectionDriver
|
||||
.safeItem(at: $0)?
|
||||
.cancelPrefetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private extensions
|
||||
|
||||
private extension Array where Element: CollectionSection {
|
||||
func attachTo(_ collectionView: UICollectionView?, driver: CollectionDriver? = nil) {
|
||||
if let collectionView = collectionView {
|
||||
enumerated().forEach {
|
||||
$1.attach(to: collectionView, at: $0)
|
||||
$1.driver = driver
|
||||
}
|
||||
} else {
|
||||
forEach { $0.detach() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
public class CollectionFlowDelegate: CollectionDelegate, UICollectionViewDelegateFlowLayout {
|
||||
|
||||
public var defaultLayout: Layout
|
||||
private var layouts: [CollectionSection: Layout] = [:]
|
||||
|
||||
var flowLayout: UICollectionViewFlowLayout {
|
||||
guard let layout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout else {
|
||||
fatalError("CollectionFlowDelegate can manage only collections with flow layout")
|
||||
}
|
||||
return layout
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public init(driver: CollectionDriver, defaultLayout: Layout = Layout()) {
|
||||
self.defaultLayout = defaultLayout
|
||||
super.init(driver: driver)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Section Layouts
|
||||
|
||||
public func layoutForSection(_ section: CollectionSection) -> Layout {
|
||||
if let layout = layouts[section] {
|
||||
return layout
|
||||
}
|
||||
|
||||
layouts[section] = defaultLayout
|
||||
return defaultLayout
|
||||
}
|
||||
|
||||
func layoutForSection(_ index: Int) -> Layout {
|
||||
return layoutForSection(collectionDriver.sections[index])
|
||||
}
|
||||
|
||||
public func configureLayout(for section: CollectionSection, closure: (inout Layout) -> Void) {
|
||||
layouts[section] = layoutForSection(section).with(closure)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Delegate
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
|
||||
let layout = layoutForSection(indexPath.section)
|
||||
let constraint = layout.itemConstraints(layout: flowLayout)
|
||||
if collectionDriver.isShowingSkeleton {
|
||||
if let itemSizeFor = collectionDriver.sections[indexPath.section].skeletonSizeForConstraint {
|
||||
let itemSize = itemSizeFor(constraint)
|
||||
return itemSize
|
||||
} else {
|
||||
return CGSize.zero
|
||||
}
|
||||
} else {
|
||||
let itemSize = collectionDriver.item(at: indexPath).sizeForConstraint(constraint)
|
||||
return itemSize
|
||||
}
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
|
||||
return layoutForSection(section).lineSpacing
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
|
||||
return layoutForSection(section).interitemSpacing
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
|
||||
return layoutForSection(section).insets
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
|
||||
|
||||
let constraint = layoutForSection(section).supplementConstraints(layout: flowLayout)
|
||||
return collectionDriver.sections[section].header?.sizeForConstraint(constraint) ?? .zero
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
|
||||
|
||||
let constraint = layoutForSection(section).supplementConstraints(layout: flowLayout)
|
||||
return collectionDriver.sections[section].footer?.sizeForConstraint(constraint) ?? .zero
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
extension CollectionFlowDelegate {
|
||||
|
||||
public struct Layout: Then {
|
||||
public var itemsPerRow: Int = 1
|
||||
public var lineSpacing: CGFloat = 0
|
||||
public var interitemSpacing: CGFloat = 0
|
||||
public var insets: UIEdgeInsets = .zero
|
||||
|
||||
public init() {}
|
||||
|
||||
func supplementConstraints(layout: UICollectionViewFlowLayout) -> CGSize {
|
||||
switch layout.scrollDirection {
|
||||
case .vertical: return CGSize(layout.collectionViewWidth, 0)
|
||||
case .horizontal: return CGSize(0, layout.collectionViewHeight)
|
||||
}
|
||||
}
|
||||
|
||||
func itemConstraints(layout: UICollectionViewFlowLayout) -> CGSize {
|
||||
switch layout.scrollDirection {
|
||||
case .vertical:
|
||||
let width = (layout.collectionViewWidth - (
|
||||
CGFloat(itemsPerRow - 1) * interitemSpacing
|
||||
+ insets.left
|
||||
+ insets.right
|
||||
)) / CGFloat(itemsPerRow)
|
||||
return CGSize(width, 0)
|
||||
|
||||
case .horizontal:
|
||||
let height = (layout.collectionViewHeight - (
|
||||
CGFloat(itemsPerRow - 1) * interitemSpacing
|
||||
+ insets.top
|
||||
+ insets.bottom
|
||||
)) / CGFloat(itemsPerRow)
|
||||
return CGSize(0, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: -
|
||||
|
||||
public extension CollectionSection {
|
||||
public var header: CollectionSupplement? {
|
||||
get {
|
||||
return supplementaryItems[UICollectionElementKindSectionHeader]?.first
|
||||
}
|
||||
set {
|
||||
supplementaryItems[UICollectionElementKindSectionHeader]
|
||||
= newValue == nil ? [] : [newValue!]
|
||||
}
|
||||
}
|
||||
|
||||
public var footer: CollectionSupplement? {
|
||||
get {
|
||||
return supplementaryItems[UICollectionElementKindSectionFooter]?.first
|
||||
}
|
||||
set {
|
||||
supplementaryItems[UICollectionElementKindSectionFooter]
|
||||
= newValue == nil ? [] : [newValue!]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UICollectionViewFlowLayout {
|
||||
var collectionViewWidth: CGFloat { return collectionView?.bounds.width ?? 0 }
|
||||
var collectionViewHeight: CGFloat { return collectionView?.bounds.height ?? 0 }
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
public class CollectionItem: Hashable {
|
||||
|
||||
private(set) public var data: AnyHashable
|
||||
private let cellClass: AnyClass
|
||||
var indexPath: IndexPath?
|
||||
weak var collectionView: UICollectionView?
|
||||
|
||||
|
||||
// MARK: - Blocks
|
||||
|
||||
var dequeueCell: () -> UICollectionViewCell = { fatalError() }
|
||||
var fillCell: () -> Void = {}
|
||||
let prefetch: () -> Void
|
||||
let cancelPrefetch: () -> Void
|
||||
let willDisplay: (UICollectionViewCell) -> Void
|
||||
let didSelect: (UICollectionViewCell) -> Void
|
||||
var sizeForConstraint: (CGSize) -> CGSize = { _ in fatalError() }
|
||||
|
||||
public init<T: CollectionItemProtocol>(_ controller: T) {
|
||||
cellClass = T.Cell.self
|
||||
data = AnyHashable(controller.data)
|
||||
|
||||
willDisplay = {
|
||||
if let cell = $0 as? T.Cell {
|
||||
controller.cellWillDisplay(cell)
|
||||
}
|
||||
}
|
||||
|
||||
didSelect = {
|
||||
if let cell = $0 as? T.Cell {
|
||||
controller.cellSelected(cell)
|
||||
}
|
||||
}
|
||||
|
||||
prefetch = controller.prefetchContent
|
||||
cancelPrefetch = controller.cancelPrefetchContent
|
||||
|
||||
dequeueCell = { [unowned self] in
|
||||
guard let indexPath = self.indexPath,
|
||||
let collectionView = self.collectionView
|
||||
else { fatalError() }
|
||||
|
||||
let cell: T.Cell = collectionView.dequeueCell(for: indexPath)
|
||||
controller.fillCell(cell, animated: false)
|
||||
return cell
|
||||
}
|
||||
|
||||
fillCell = { [weak self] in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let `self` = self, let indexPath = self.indexPath, let collectionView = self.collectionView else { return }
|
||||
if let cell = collectionView.cellForItem(at: indexPath) as? T.Cell {
|
||||
controller.fillCell(cell, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sizeForConstraint = {
|
||||
return controller.size(constrainedTo: $0.width, height: $0.height)
|
||||
}
|
||||
|
||||
controller.events.dataUpdated = { [unowned self] newData in
|
||||
self.data = newData
|
||||
self.fillCell()
|
||||
}
|
||||
|
||||
controller.events.dataAndSizeUpdated = { [unowned self] newData in
|
||||
self.data = newData
|
||||
guard let indexPath = self.indexPath,
|
||||
let collectionView = self.collectionView
|
||||
else { return }
|
||||
UIView.performWithoutAnimation {
|
||||
collectionView.reloadItems(at: [ indexPath ])
|
||||
}
|
||||
}
|
||||
|
||||
controller.events.animatedSizeUpdated = { [unowned self] in
|
||||
self.collectionView?.performBatchUpdates({}, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
public var hashValue: Int {
|
||||
return data.hashValue
|
||||
}
|
||||
|
||||
public static func == (lhs: CollectionItem, rhs: CollectionItem) -> Bool {
|
||||
return lhs.cellClass == rhs.cellClass && lhs.data == rhs.data
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
public struct CollectionItemEvents<Data> {
|
||||
public var dataUpdated: (Data) -> Void = { _ in }
|
||||
public var dataAndSizeUpdated: (Data) -> Void = { _ in }
|
||||
public var animatedSizeUpdated: () -> Void = { }
|
||||
public init() { }
|
||||
}
|
||||
|
||||
public protocol CollectionItemProtocol: class {
|
||||
associatedtype Cell: UICollectionViewCell
|
||||
associatedtype Data: Hashable
|
||||
|
||||
// Data-related
|
||||
var data: Data { get }
|
||||
|
||||
// Cell-related
|
||||
func fillCell(_ cell: Cell, animated: Bool)
|
||||
func size(constrainedTo width: CGFloat, height: CGFloat) -> CGSize
|
||||
func cellWillDisplay(_ cell: Cell)
|
||||
func cellSelected(_ cell: Cell)
|
||||
|
||||
// Events to pass to collection driver
|
||||
var events: CollectionItemEvents<Data> { get set }
|
||||
|
||||
// Prefetching
|
||||
func prefetchContent()
|
||||
func cancelPrefetchContent()
|
||||
}
|
||||
|
||||
public extension CollectionItemProtocol {
|
||||
static func registerReusableCell(in collectionView: UICollectionView) {
|
||||
collectionView.registerCell(Cell.self)
|
||||
}
|
||||
|
||||
var collectionItem: CollectionItem {
|
||||
return CollectionItem(self)
|
||||
}
|
||||
|
||||
func singleItemSection(_ identifier: String = ProcessInfo.processInfo.globallyUniqueString) -> CollectionSection {
|
||||
return CollectionSection(identifier: identifier, items: [ self.collectionItem ])
|
||||
}
|
||||
|
||||
// MARK: - Default implementations
|
||||
|
||||
func prefetchContent() { }
|
||||
func cancelPrefetchContent() { }
|
||||
func cellWillDisplay(_ cell: Cell) { }
|
||||
func cellSelected(_ cell: Cell) { }
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import UIKit
|
||||
import VendefyService
|
||||
|
||||
final class SingleCellController<CollectionCell: UICollectionViewCell & Fillable, ItemData>: CollectionItemProtocol where CollectionCell.Data == ItemData {
|
||||
|
||||
typealias Cell = CollectionCell
|
||||
typealias Data = ItemData
|
||||
|
||||
var cellSelected: ((Cell, Data) -> ())?
|
||||
var cellConfigure: ((Cell) -> ())?
|
||||
|
||||
var data: ItemData
|
||||
var events = CollectionItemEvents<Data>()
|
||||
|
||||
let height: CGFloat
|
||||
|
||||
init(data: ItemData, height: CGFloat) {
|
||||
self.data = data
|
||||
self.height = height
|
||||
}
|
||||
|
||||
// MARK: CollectionItemProtocol
|
||||
func fillCell(_ cell: CollectionCell, animated: Bool) {
|
||||
cell.fill(data)
|
||||
cellConfigure?(cell)
|
||||
}
|
||||
|
||||
func size(constrainedTo width: CGFloat, height: CGFloat) -> CGSize {
|
||||
return CGSize(width: width, height: self.height)
|
||||
}
|
||||
|
||||
func cellSelected(_ cell: Cell) {
|
||||
cellSelected?(cell, data)
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import UIKit
|
||||
import SkeletonView
|
||||
|
||||
public class CollectionSection: Hashable, Then {
|
||||
|
||||
private var sectionIndex: Int?
|
||||
private weak var collectionView: UICollectionView?
|
||||
weak var driver: CollectionDriver?
|
||||
|
||||
|
||||
// MARK: - Data
|
||||
|
||||
public var items: [CollectionItem] {
|
||||
didSet {
|
||||
oldValue.attachTo(nil, sectionIndex: nil)
|
||||
items.attachTo(collectionView, sectionIndex: sectionIndex)
|
||||
|
||||
if let collectionView = collectionView,
|
||||
let sectionIndex = sectionIndex,
|
||||
let driver = driver
|
||||
{
|
||||
driver.enqueueDiff {
|
||||
// Call fill for visible cells so bindings are set because
|
||||
// CollectionItem are always recreated.
|
||||
collectionView.indexPathsForVisibleItems.forEach { indexPath in
|
||||
guard indexPath.section == sectionIndex else { return }
|
||||
|
||||
if indexPath.item < self.items.endIndex {
|
||||
self.items[indexPath.item].fillCell()
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate
|
||||
let changes = diff(old: oldValue, new: self.items)
|
||||
return .items(changes, section: sectionIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var supplementaryItems: [String: [CollectionSupplement]] = [:] {
|
||||
didSet {
|
||||
supplementaryItems.values.forEach {
|
||||
$0.attachTo(collectionView, sectionIndex: sectionIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public let identifier: String
|
||||
|
||||
|
||||
// MARK: - Skeleton
|
||||
|
||||
public var skeletonCellIdentifier: ReusableCellIdentifier?
|
||||
public var skeletonSizeForConstraint: ((CGSize) -> CGSize)?
|
||||
public var skeletonCellsCount: Int?
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public init(identifier: String = ProcessInfo.processInfo.globallyUniqueString, items: [CollectionItem] = []) {
|
||||
self.identifier = identifier
|
||||
self.items = items
|
||||
}
|
||||
|
||||
|
||||
// MARK: -
|
||||
|
||||
func attach(to collectionView: UICollectionView, at index: Int) {
|
||||
self.collectionView = collectionView
|
||||
self.sectionIndex = index
|
||||
items.attachTo(collectionView, sectionIndex: index)
|
||||
supplementaryItems.values.forEach {
|
||||
$0.attachTo(collectionView, sectionIndex: index)
|
||||
}
|
||||
}
|
||||
|
||||
func detach() {
|
||||
collectionView = nil
|
||||
sectionIndex = nil
|
||||
items.attachTo(nil, sectionIndex: nil)
|
||||
supplementaryItems.values.forEach {
|
||||
$0.attachTo(nil, sectionIndex: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
public var hashValue: Int {
|
||||
return identifier.hashValue
|
||||
}
|
||||
|
||||
public static func == (lhs: CollectionSection, rhs: CollectionSection) -> Bool {
|
||||
return lhs.identifier == rhs.identifier && lhs.items == rhs.items
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: -
|
||||
|
||||
private extension Array where Element: CollectionItem {
|
||||
func attachTo(_ collectionView: UICollectionView?, sectionIndex: Int?) {
|
||||
enumerated().forEach { index, item in
|
||||
item.collectionView = collectionView
|
||||
item.indexPath = sectionIndex.map { IndexPath(item: index, section: $0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array where Element: CollectionSupplement {
|
||||
func attachTo(_ collectionView: UICollectionView?, sectionIndex: Int?) {
|
||||
enumerated().forEach { index, item in
|
||||
item.collectionView = collectionView
|
||||
item.indexPath = sectionIndex.map { IndexPath(item: index, section: $0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
public class CollectionSupplement {
|
||||
|
||||
var indexPath: IndexPath?
|
||||
weak var collectionView: UICollectionView?
|
||||
|
||||
// MARK: - Blocks
|
||||
|
||||
var sizeForConstraint: (CGSize) -> CGSize = { _ in fatalError() }
|
||||
var dequeueView: () -> UICollectionReusableView = { fatalError() }
|
||||
var didScroll: (UIScrollView) -> () = { _ in }
|
||||
|
||||
// MARK: -
|
||||
|
||||
public init<T: CollectionSupplementProtocol>(_ controller: T) {
|
||||
sizeForConstraint = {
|
||||
return controller.sizeForConstraint($0)
|
||||
}
|
||||
|
||||
dequeueView = { [unowned self] in
|
||||
guard let collectionView = self.collectionView,
|
||||
let indexPath = self.indexPath
|
||||
else { fatalError() }
|
||||
|
||||
let view: T.View = collectionView.dequeueSupplement(kind: T.kind, for: indexPath)
|
||||
controller.fill(view)
|
||||
return view
|
||||
}
|
||||
|
||||
didScroll = { scrollView in controller.didScroll(scrollView: scrollView) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
public protocol CollectionSupplementProtocol {
|
||||
associatedtype View: UICollectionReusableView
|
||||
static var kind: String { get }
|
||||
func sizeForConstraint(_ constraint: CGSize) -> CGSize
|
||||
func fill(_ view: View)
|
||||
func didScroll(scrollView: UIScrollView)
|
||||
}
|
||||
|
||||
public extension CollectionSupplementProtocol {
|
||||
static func registerReusable(in collectionView: UICollectionView) {
|
||||
collectionView.registerSupplement(View.self, kind: kind)
|
||||
}
|
||||
|
||||
var supplementaryItem: CollectionSupplement {
|
||||
return CollectionSupplement(self)
|
||||
}
|
||||
|
||||
// default implementation
|
||||
func didScroll(scrollView: UIScrollView) { }
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
public enum Haptic {
|
||||
case impact(UIImpactFeedbackStyle)
|
||||
case notification(UINotificationFeedbackType)
|
||||
case impact(UIImpactFeedbackGenerator.FeedbackStyle)
|
||||
case notification(UINotificationFeedbackGenerator.FeedbackType)
|
||||
case selection
|
||||
|
||||
public func generate() {
|
||||
|
||||
@@ -29,7 +29,7 @@ extension Hapticable where Self: UIButton {
|
||||
set { setAssociatedObject(&hapticKey, newValue) }
|
||||
}
|
||||
|
||||
public var hapticControlEvents: UIControlEvents? {
|
||||
public var hapticControlEvents: UIControl.Event? {
|
||||
get { return getAssociatedObject(&eventKey) }
|
||||
set { setAssociatedObject(&eventKey, newValue) }
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public extension SwiftyImageView {
|
||||
case flipFromTop(TimeInterval)
|
||||
case custom(
|
||||
duration: TimeInterval,
|
||||
animationOptions: UIViewAnimationOptions,
|
||||
animationOptions: UIView.AnimationOptions,
|
||||
animations: (SwiftyImageView, UIImage) -> Void,
|
||||
completion: ((Bool) -> Void)?
|
||||
)
|
||||
@@ -67,10 +67,10 @@ public extension SwiftyImageView {
|
||||
}
|
||||
|
||||
/// The animation options of the image transition.
|
||||
public var animationOptions: UIViewAnimationOptions {
|
||||
public var animationOptions: UIView.AnimationOptions {
|
||||
switch self {
|
||||
case .noTransition:
|
||||
return UIViewAnimationOptions()
|
||||
return UIView.AnimationOptions()
|
||||
case .crossDissolve:
|
||||
return .transitionCrossDissolve
|
||||
case .curlDown:
|
||||
|
||||
@@ -43,6 +43,11 @@ public extension UIControl {
|
||||
return getOrCreateSignalForUIControlEvent(.touchUpInside);
|
||||
}
|
||||
|
||||
/// A signal that fires for each primary action control event.
|
||||
public var onPrimaryActionTriggered: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.primaryActionTriggered);
|
||||
}
|
||||
|
||||
/// A signal that fires for each touch up outside control event.
|
||||
public var onTouchUpOutside: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.touchUpOutside);
|
||||
@@ -84,7 +89,7 @@ public extension UIControl {
|
||||
static var SignalDictionaryKey = "signals_signalKey"
|
||||
}
|
||||
|
||||
private static let eventToKey: [UIControlEvents: NSString] = [
|
||||
private static let eventToKey: [UIControl.Event: NSString] = [
|
||||
.touchDown: "TouchDown",
|
||||
.touchDownRepeat: "TouchDownRepeat",
|
||||
.touchDragInside: "TouchDragInside",
|
||||
@@ -92,6 +97,7 @@ public extension UIControl {
|
||||
.touchDragEnter: "TouchDragEnter",
|
||||
.touchDragExit: "TouchDragExit",
|
||||
.touchUpInside: "TouchUpInside",
|
||||
.primaryActionTriggered: "PrimaryActionTriggered",
|
||||
.touchUpOutside: "TouchUpOutside",
|
||||
.touchCancel: "TouchCancel",
|
||||
.valueChanged: "ValueChanged",
|
||||
@@ -100,7 +106,7 @@ public extension UIControl {
|
||||
.editingDidEnd: "EditingDidEnd",
|
||||
.editingDidEndOnExit: "EditingDidEndOnExit"]
|
||||
|
||||
private func getOrCreateSignalForUIControlEvent(_ event: UIControlEvents) -> Signal<(Void)> {
|
||||
private func getOrCreateSignalForUIControlEvent(_ event: UIControl.Event) -> Signal<(Void)> {
|
||||
guard let key = UIControl.eventToKey[event] else {
|
||||
assertionFailure("Event type is not handled")
|
||||
return Signal()
|
||||
@@ -117,7 +123,7 @@ public extension UIControl {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleUIControlEvent(_ uiControlEvent: UIControlEvents) {
|
||||
private func handleUIControlEvent(_ uiControlEvent: UIControl.Event) {
|
||||
getOrCreateSignalForUIControlEvent(uiControlEvent).fire(())
|
||||
}
|
||||
|
||||
@@ -148,6 +154,10 @@ public extension UIControl {
|
||||
@objc private dynamic func eventHandlerTouchUpInside() {
|
||||
handleUIControlEvent(.touchUpInside)
|
||||
}
|
||||
|
||||
@objc private dynamic func eventHandlerPrimaryActionTriggered() {
|
||||
handleUIControlEvent(.primaryActionTriggered)
|
||||
}
|
||||
|
||||
@objc private dynamic func eventHandlerTouchUpOutside() {
|
||||
handleUIControlEvent(.touchUpOutside)
|
||||
@@ -178,7 +188,7 @@ public extension UIControl {
|
||||
}
|
||||
}
|
||||
|
||||
extension UIControlEvents: Hashable {
|
||||
extension UIControl.Event: Hashable {
|
||||
public var hashValue: Int {
|
||||
return Int(self.rawValue)
|
||||
}
|
||||
|
||||
@@ -73,33 +73,8 @@ extension Collection where Self.Index == Self.Indices.Iterator.Element {
|
||||
}
|
||||
}
|
||||
|
||||
public extension Collection {
|
||||
|
||||
/**
|
||||
Creats a shuffled version of this array using the Fisher-Yates (fast and uniform) shuffle.
|
||||
- returns: A shuffled version of this array.
|
||||
*/
|
||||
public func shuffled() -> [Iterator.Element] {
|
||||
var list = Array(self)
|
||||
list.shuffle()
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
public extension MutableCollection where Index == Int {
|
||||
/**
|
||||
Shuffle the array using the Fisher-Yates (fast and uniform) shuffle. Mutating.
|
||||
*/
|
||||
public mutating func shuffle() {
|
||||
// Empty and single-element collections don't shuffle.
|
||||
guard count > 1 else { return }
|
||||
|
||||
for i in 0 ..< (count - 1) {
|
||||
let j = Int(arc4random_uniform(UInt32(count - i))) + i
|
||||
guard i != j else { continue }
|
||||
self.swapAt(i, j)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a random element from the collection.
|
||||
|
||||
@@ -24,7 +24,6 @@ public extension Int {
|
||||
/// - Returns: Returns a random Int point number between 0 and n max
|
||||
public static func random(min: Int, max: Int) -> Int {
|
||||
return Int.random(n: max - min + 1) + min
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ extension String {
|
||||
size = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
||||
}
|
||||
|
||||
let attributes: [NSAttributedStringKey: Any] = [ NSAttributedStringKey.font: font ]
|
||||
let attributes: [NSAttributedString.Key: Any] = [ NSAttributedString.Key.font: font ]
|
||||
let result = (self as NSString).boundingRect(with: size,options: [ .usesLineFragmentOrigin ], attributes: attributes, context: nil).size
|
||||
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
protocol InteractiveDismiss: class {
|
||||
|
||||
var canBeginInteractiveDismiss: Bool { get }
|
||||
var backgroundView: UIView { get }
|
||||
var contentView: UIView { get }
|
||||
|
||||
func setupInteractiveDismission()
|
||||
func viewDidCompleteInteractiveDismiss()
|
||||
func viewPossiblyWillDissmiss()
|
||||
}
|
||||
|
||||
private struct AssociatedKeys {
|
||||
static var exceededThreshold: Int = 0
|
||||
static var totalTranslation: Int = 1
|
||||
static var gestureDelegate: Int = 2
|
||||
}
|
||||
|
||||
extension InteractiveDismiss where Self: UIViewController {
|
||||
|
||||
private var exceededDismissionThreshold: Bool {
|
||||
get { return getAssociatedObject(&AssociatedKeys.exceededThreshold) ?? false }
|
||||
set { setAssociatedObject(&AssociatedKeys.exceededThreshold, newValue) }
|
||||
}
|
||||
|
||||
private var totalTranslationY: CGFloat {
|
||||
get { return getAssociatedObject(&AssociatedKeys.totalTranslation) ?? 0 }
|
||||
set { setAssociatedObject(&AssociatedKeys.totalTranslation, newValue) }
|
||||
}
|
||||
|
||||
private var gestureDelegate: GestureRecognizerDelegate? {
|
||||
get { return getAssociatedObject(&AssociatedKeys.gestureDelegate) }
|
||||
set { setAssociatedObject(&AssociatedKeys.gestureDelegate, newValue) }
|
||||
}
|
||||
|
||||
func dismissAnimated() {
|
||||
animateExit()
|
||||
}
|
||||
|
||||
func setupInteractiveDismission() {
|
||||
|
||||
let panGesture = UIPanGestureRecognizer {[weak self] recognizer in
|
||||
self?.didPanView(panGesture: recognizer as! UIPanGestureRecognizer)
|
||||
}
|
||||
panGesture.cancelsTouchesInView = false
|
||||
let delegate = GestureRecognizerDelegate()
|
||||
delegate.recognizerShouldBegin = { [unowned self] _ in return self.canBeginInteractiveDismiss }
|
||||
panGesture.delegate = delegate
|
||||
gestureDelegate = delegate
|
||||
view.addGestureRecognizer(panGesture)
|
||||
}
|
||||
|
||||
private func didPanView(panGesture: UIPanGestureRecognizer) {
|
||||
|
||||
// Update translation
|
||||
let translation = panGesture.translation(in: view)
|
||||
|
||||
// Check which view should update
|
||||
if contentView.frame.origin.y + translation.y > 0 {
|
||||
|
||||
// We are scrolling down
|
||||
totalTranslationY += translation.y
|
||||
|
||||
// Check threshold
|
||||
if totalTranslationY > Theme.scrollDownSlopThreshold {
|
||||
contentView.frame.origin.y += translation.y
|
||||
|
||||
// Updated the view behind this with the proper index
|
||||
// Prevents a jitter when exiting
|
||||
if !exceededDismissionThreshold {
|
||||
exceededDismissionThreshold = true
|
||||
viewPossiblyWillDissmiss()
|
||||
}
|
||||
} else {
|
||||
contentView.frame.origin.y = 0
|
||||
}
|
||||
|
||||
// Perform changes
|
||||
performTranslationBasedStyles()
|
||||
}
|
||||
|
||||
// Handle extras
|
||||
switch panGesture.state {
|
||||
case .ended:
|
||||
if totalTranslationY > Theme.scrollDownSlopThreshold {
|
||||
|
||||
// Determine velocity
|
||||
let velocity = panGesture.velocity(in: view)
|
||||
let projectedY = Inertia.project(initialVelocity: velocity.y)
|
||||
let duration = TimeInterval(view.frame.size.height / velocity.y)
|
||||
|
||||
// Handle case
|
||||
if projectedY >= Theme.flingToDismissVelocityThreshold {
|
||||
animateExit(duration, projectedY)
|
||||
} else if contentView.frame.origin.y >= view.frame.height * 0.4 {
|
||||
animateExit()
|
||||
} else {
|
||||
let springVelocity = projectedY / view.frame.height
|
||||
animateBack(intensity: springVelocity)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset
|
||||
totalTranslationY = 0
|
||||
exceededDismissionThreshold = false
|
||||
default: break
|
||||
}
|
||||
|
||||
// Reset
|
||||
panGesture.setTranslation(.zero, in: view)
|
||||
}
|
||||
|
||||
private func animateExit(_ duration: TimeInterval = 0.33, _ projectedY: CGFloat = 0) {
|
||||
UIView.animate(withDuration: duration, animations: {
|
||||
self.contentView.frame.origin.y = self.view.frame.height + projectedY
|
||||
self.backgroundView.alpha = 0
|
||||
}) { _ in
|
||||
self.viewDidCompleteInteractiveDismiss()
|
||||
self.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func animateBack(intensity: CGFloat) {
|
||||
let velocity = intensity.limited(1, 1 + intensity)
|
||||
UIView.animate(withDuration: 0.33, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity, options: .allowUserInteraction, animations: {
|
||||
self.contentView.frame.origin.y = 0
|
||||
self.backgroundView.alpha = 1
|
||||
}, completion: nil)
|
||||
}
|
||||
|
||||
// Changes styles based on the drag og the content view
|
||||
private func performTranslationBasedStyles() {
|
||||
let translation = 1 - (contentView.frame.origin.y / view.frame.height)
|
||||
backgroundView.alpha = translation
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Theme
|
||||
private enum Theme {
|
||||
static let scrollDownSlopThreshold: CGFloat = 36
|
||||
static let flingToDismissVelocityThreshold: CGFloat = 150
|
||||
}
|
||||
@@ -88,7 +88,7 @@ public final class ScaleModalDismissAC: NSObject, UIViewControllerAnimatedTransi
|
||||
|
||||
}, completion: { _ in
|
||||
fromVC.view.removeFromSuperview()
|
||||
fromVC.removeFromParentViewController()
|
||||
fromVC.removeFromParent()
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import UIKit.UIGestureRecognizerSubclass
|
||||
|
||||
|
||||
protocol TouchInteraction: class {
|
||||
func setupDefaultTouchInteraction()
|
||||
}
|
||||
|
||||
|
||||
extension UIView: TouchInteraction {}
|
||||
|
||||
|
||||
extension TouchInteraction where Self: UIView {
|
||||
|
||||
func setupDefaultTouchInteraction() {
|
||||
|
||||
let recognizer = TouchInteractionRecognizer { recognizer in
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
UIView.animate(withDuration: 0.1, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: {
|
||||
self.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
|
||||
self.alpha = 0.6
|
||||
})
|
||||
case .ended, .failed, .cancelled:
|
||||
UIView.animate(withDuration: 0.175, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: {
|
||||
self.transform = CGAffineTransform(scaleX: 1, y: 1)
|
||||
self.alpha = 1
|
||||
})
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
self.addGestureRecognizer(recognizer)
|
||||
self.isUserInteractionEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TouchInteractionRecognizer: UIGestureRecognizer {
|
||||
|
||||
override init(target: Any?, action: Selector?) {
|
||||
super.init(target: target, action: action)
|
||||
delaysTouchesBegan = false
|
||||
delaysTouchesEnded = false
|
||||
cancelsTouchesInView = false
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
if touches.count != 1 {
|
||||
state = .failed
|
||||
}
|
||||
state = .began
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
if touches.count != 1 {
|
||||
state = .failed
|
||||
}
|
||||
state = .ended
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
state = .cancelled
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
state = .possible
|
||||
}
|
||||
}
|
||||
@@ -25,25 +25,25 @@ extension UIBarButtonItem {
|
||||
get { return objc_getAssociatedObject(self, &AssociatedKeys.ActionName) as? Action }
|
||||
}
|
||||
|
||||
public convenience init(image: UIImage?, style: UIBarButtonItemStyle = .plain, action: @escaping () -> Void) {
|
||||
public convenience init(image: UIImage?, style: UIBarButtonItem.Style = .plain, action: @escaping () -> Void) {
|
||||
let handler = Action(action: action)
|
||||
self.init(image: image, style: style, target: handler, action: #selector(Action.handleAction(_:)))
|
||||
barButtonAction = handler
|
||||
}
|
||||
|
||||
public convenience init(title: String?, style: UIBarButtonItemStyle = .plain, action: @escaping () -> Void) {
|
||||
public convenience init(title: String?, style: UIBarButtonItem.Style = .plain, action: @escaping () -> Void) {
|
||||
let handler = Action(action: action)
|
||||
self.init(title: title, style: style, target: handler, action: #selector(Action.handleAction(_:)))
|
||||
barButtonAction = handler
|
||||
}
|
||||
|
||||
public convenience init(barButtonSystemItem: UIBarButtonSystemItem, action: @escaping () -> Void) {
|
||||
public convenience init(barButtonSystemItem: UIBarButtonItem.SystemItem, action: @escaping () -> Void) {
|
||||
let handler = Action(action: action)
|
||||
self.init(barButtonSystemItem: barButtonSystemItem, target: handler, action: #selector(Action.handleAction(_:)))
|
||||
barButtonAction = handler
|
||||
}
|
||||
|
||||
public convenience init(image: UIImage?, landscapeImagePhone: UIImage?, style: UIBarButtonItemStyle, action: @escaping () -> Void) {
|
||||
public convenience init(image: UIImage?, landscapeImagePhone: UIImage?, style: UIBarButtonItem.Style, action: @escaping () -> Void) {
|
||||
let handler = Action(action: action)
|
||||
self.init(image: image, landscapeImagePhone: landscapeImagePhone, style: style, target: handler, action: #selector(Action.handleAction(_:)))
|
||||
barButtonAction = handler
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class HorizontalAlignmentLayout: UICollectionViewFlowLayout {
|
||||
|
||||
// KVO
|
||||
fileprivate struct KVO {
|
||||
|
||||
var keyPath: String
|
||||
var options: NSKeyValueObservingOptions
|
||||
var context: Int
|
||||
|
||||
static var contentOffset = KVO(
|
||||
keyPath: #keyPath(UICollectionViewFlowLayout.collectionView.contentOffset),
|
||||
options: .new,
|
||||
context: 0
|
||||
)
|
||||
|
||||
static var bounds = KVO(
|
||||
keyPath: #keyPath(UICollectionViewFlowLayout.collectionView.bounds),
|
||||
options: .new,
|
||||
context: 1
|
||||
)
|
||||
}
|
||||
|
||||
public enum Alignment {
|
||||
case left
|
||||
case center
|
||||
case right
|
||||
}
|
||||
|
||||
// Properties
|
||||
public var currentPage: Int?
|
||||
public var currentPageDidChange: (Int) -> Void = { _ in }
|
||||
let alignment: Alignment
|
||||
|
||||
var collectionWidth: CGFloat {
|
||||
guard let collection = collectionView else { return 0 }
|
||||
return collection.bounds.size.width - collection.contentInset.right - collection.contentInset.left
|
||||
}
|
||||
|
||||
deinit {
|
||||
removeObserver(self, forKeyPath: KVO.bounds.keyPath, context: &KVO.bounds.context)
|
||||
}
|
||||
|
||||
public init(itemSize: CGSize, spacing: CGFloat, alignment: Alignment) {
|
||||
self.alignment = alignment
|
||||
super.init()
|
||||
self.itemSize = itemSize
|
||||
self.scrollDirection = .horizontal
|
||||
self.minimumLineSpacing = spacing
|
||||
|
||||
addObserver(self,
|
||||
forKeyPath: KVO.bounds.keyPath,
|
||||
options: KVO.bounds.options,
|
||||
context: &KVO.bounds.context)
|
||||
|
||||
addObserver(self,
|
||||
forKeyPath: KVO.contentOffset.keyPath,
|
||||
options: KVO.contentOffset.options,
|
||||
context: &KVO.contentOffset.context)
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
}
|
||||
|
||||
// MARK: override
|
||||
extension HorizontalAlignmentLayout {
|
||||
|
||||
public override func prepare() {
|
||||
switch alignment {
|
||||
case .left:
|
||||
sectionInset.left = 0
|
||||
sectionInset.right = collectionWidth - itemSize.width
|
||||
case .center:
|
||||
let inset = (collectionWidth - itemSize.width) / 2
|
||||
sectionInset.left = inset
|
||||
sectionInset.right = inset
|
||||
case .right:
|
||||
sectionInset.right = 0
|
||||
sectionInset.left = collectionWidth - itemSize.width
|
||||
}
|
||||
super.prepare()
|
||||
}
|
||||
|
||||
public override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
|
||||
|
||||
guard let collectionView = self.collectionView else {
|
||||
return proposedContentOffset
|
||||
}
|
||||
|
||||
let proposedRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.bounds.width, height: collectionView.bounds.height)
|
||||
|
||||
guard let layoutAttributes = self.layoutAttributesForElements(in: proposedRect) else {
|
||||
return proposedContentOffset
|
||||
}
|
||||
|
||||
var candidateAttributes: UICollectionViewLayoutAttributes?
|
||||
let proposedContentOffsetX: CGFloat
|
||||
switch alignment {
|
||||
case .left:
|
||||
proposedContentOffsetX = proposedContentOffset.x + itemSize.width / 2
|
||||
case .center:
|
||||
proposedContentOffsetX = proposedContentOffset.x + collectionView.bounds.width / 2
|
||||
case .right:
|
||||
proposedContentOffsetX = proposedContentOffset.x + collectionView.bounds.width
|
||||
}
|
||||
|
||||
for attributes in layoutAttributes {
|
||||
guard attributes.representedElementCategory == .cell else { continue }
|
||||
|
||||
if candidateAttributes == nil {
|
||||
candidateAttributes = attributes
|
||||
continue
|
||||
}
|
||||
|
||||
let attributePosition: CGFloat
|
||||
let candidatePosition: CGFloat
|
||||
switch alignment {
|
||||
case .left:
|
||||
attributePosition = attributes.frame.minX
|
||||
candidatePosition = candidateAttributes!.frame.minX
|
||||
case .center:
|
||||
attributePosition = attributes.center.x
|
||||
candidatePosition = candidateAttributes!.center.x
|
||||
case .right:
|
||||
attributePosition = attributes.frame.maxX
|
||||
candidatePosition = candidateAttributes!.frame.maxX
|
||||
}
|
||||
|
||||
if abs(attributePosition - proposedContentOffsetX) < abs(candidatePosition - proposedContentOffsetX) {
|
||||
candidateAttributes = attributes
|
||||
}
|
||||
}
|
||||
|
||||
guard let aCandidateAttributes = candidateAttributes else {
|
||||
return proposedContentOffset
|
||||
}
|
||||
|
||||
var newOffsetX: CGFloat
|
||||
switch alignment {
|
||||
case .left:
|
||||
newOffsetX = aCandidateAttributes.frame.minX - collectionView.contentInset.left
|
||||
case .center:
|
||||
newOffsetX = aCandidateAttributes.center.x - collectionView.bounds.size.width / 2
|
||||
case .right:
|
||||
newOffsetX = aCandidateAttributes.frame.minX - collectionView.bounds.size.width + itemSize.width
|
||||
}
|
||||
|
||||
let offset = newOffsetX - collectionView.contentOffset.x
|
||||
|
||||
if (velocity.x < 0 && offset > 0) || (velocity.x > 0 && offset < 0) {
|
||||
let pageWidth = itemSize.width + minimumLineSpacing
|
||||
newOffsetX += velocity.x > 0 ? pageWidth : -pageWidth
|
||||
}
|
||||
|
||||
return CGPoint(x: newOffsetX, y: proposedContentOffset.y)
|
||||
}
|
||||
|
||||
public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
switch context {
|
||||
case (&KVO.bounds.context)?:
|
||||
// TODO: Current layout makes hack for first item to appear in the center.
|
||||
// (method configure Insets). Settings insets shouldn't be done in invalidateLayout.
|
||||
// Layout should handle appear state on it's own.
|
||||
let oldValue = change?[.oldKey] as? NSValue
|
||||
let newValue = change?[.newKey] as? NSValue
|
||||
if oldValue?.cgRectValue != newValue?.cgRectValue {
|
||||
invalidateLayout()
|
||||
}
|
||||
|
||||
case (&KVO.contentOffset.context)?:
|
||||
guard let collectionView = collectionView, collectionView.frame.size != CGSize.zero else {
|
||||
return
|
||||
}
|
||||
|
||||
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)
|
||||
let layoutAttributes = layoutAttributesForElements(in: visibleRect)
|
||||
|
||||
let center: CGFloat
|
||||
switch alignment {
|
||||
case .left:
|
||||
let middle = collectionView.contentInset.left + itemSize.width / 2
|
||||
center = collectionView.contentOffset.x + middle
|
||||
case .center:
|
||||
let middle = collectionView.frame.width / 2
|
||||
center = collectionView.contentOffset.x + middle
|
||||
case .right:
|
||||
let middle = collectionView.frame.width - collectionView.contentInset.right - itemSize.width / 2
|
||||
center = collectionView.contentOffset.x + middle
|
||||
}
|
||||
|
||||
let closestAttribute = layoutAttributes?.sorted {
|
||||
abs($0.center.x - center) < abs($1.center.x - center)
|
||||
}.first
|
||||
if let closestAttribute = closestAttribute, currentPage != closestAttribute.indexPath.row {
|
||||
currentPage = closestAttribute.indexPath.row
|
||||
if let currentPage = currentPage {
|
||||
currentPageDidChange(currentPage)
|
||||
}
|
||||
}
|
||||
default:
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class PinnedHeaderFlowLayout: UICollectionViewFlowLayout {
|
||||
|
||||
private let header = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: IndexPath(row: 0, section: 0))
|
||||
private var headerSize: CGSize = .zero
|
||||
|
||||
public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
|
||||
guard let collectionView = self.collectionView else { return nil }
|
||||
let attributes = super.layoutAttributesForElements(in: rect)
|
||||
|
||||
let headerAttributes = attributes?.filter { $0.isFirstHeader }.first
|
||||
if let headerAttributes = headerAttributes {
|
||||
headerSize = headerAttributes.size
|
||||
}
|
||||
|
||||
if collectionView.contentOffset.y > 0 {
|
||||
header.frame.origin.y = collectionView.contentOffset.y
|
||||
} else {
|
||||
header.frame.origin.y = 0
|
||||
}
|
||||
|
||||
var newAttribures = attributes?.filter { !$0.isFirstHeader }
|
||||
header.frame.size = headerSize
|
||||
header.zIndex = Int.max
|
||||
newAttribures?.append(header)
|
||||
|
||||
return newAttribures
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private extension UICollectionViewLayoutAttributes {
|
||||
var isFirstHeader: Bool {
|
||||
guard let elementKind = representedElementKind else { return false }
|
||||
return (elementKind == UICollectionView.elementKindSectionHeader && indexPath.section == 0)
|
||||
}
|
||||
}
|
||||
@@ -2,60 +2,69 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
public struct DisplaySize {
|
||||
public static let iphone4 = CGSize(width: 320, height: 480)
|
||||
public static let iphone5 = CGSize(width: 320, height: 568)
|
||||
public static let iphone6 = CGSize(width: 375, height: 667)
|
||||
public static let iphone6plus = CGSize(width: 414, height: 736)
|
||||
public static let iphoneX = CGSize(width: 375, height: 812)
|
||||
public static let iPad9 = CGSize(width: 768, height: 1024)
|
||||
public static let ipad10 = CGSize(width: 834, height: 1112)
|
||||
public static let ipad12 = CGSize(width: 1024, height: 1366)
|
||||
public static let unknown = CGSize.zero
|
||||
public static let iphone4 = CGSize(width: 320, height: 480)
|
||||
public static let iphone5 = CGSize(width: 320, height: 568)
|
||||
public static let iphone6 = CGSize(width: 375, height: 667)
|
||||
public static let iphone6plus = CGSize(width: 414, height: 736)
|
||||
public static let iphoneX = CGSize(width: 375, height: 812)
|
||||
public static let iphoneXR = CGSize(width: 414, height: 896)
|
||||
public static let iPad9 = CGSize(width: 768, height: 1024)
|
||||
public static let ipad10 = CGSize(width: 834, height: 1112)
|
||||
public static let ipad12 = CGSize(width: 1024, height: 1366)
|
||||
public static let unknown = CGSize.zero
|
||||
}
|
||||
|
||||
public enum DisplayType {
|
||||
case unknown
|
||||
case iphone4
|
||||
case iphone5
|
||||
case iphone6
|
||||
case iphone6plus
|
||||
case iphoneX
|
||||
case ipad9
|
||||
case ipad10
|
||||
case ipad12
|
||||
case unknown
|
||||
case iphone4
|
||||
case iphone5
|
||||
case iphone6
|
||||
case iphone6plus
|
||||
case iphoneX
|
||||
case iphoneXR
|
||||
case iphoneXM
|
||||
case ipad9
|
||||
case ipad10
|
||||
case ipad12
|
||||
}
|
||||
|
||||
public enum Display {
|
||||
public static var width : CGFloat { return UIScreen.main.bounds.size.width }
|
||||
public static var height : CGFloat { return UIScreen.main.bounds.size.height }
|
||||
public static var maxLength : CGFloat { return max(width, height) }
|
||||
public static var minLength : CGFloat { return min(width, height) }
|
||||
public static var navbarSize : CGFloat { return Display.height == 812 ? 88 : 64 }
|
||||
public static var bottombarSize : CGFloat { return Display.height == 812 ? 34 : 0 }
|
||||
public static var zoomed : Bool { return UIScreen.main.nativeScale >= UIScreen.main.scale }
|
||||
public static var retina : Bool { return UIScreen.main.scale >= 2.0 }
|
||||
public static var phone : Bool { return UIDevice.current.userInterfaceIdiom == .phone }
|
||||
public static var pad : Bool { return UIDevice.current.userInterfaceIdiom == .pad }
|
||||
|
||||
public static var displayType: DisplayType {
|
||||
if phone && maxLength < 568 {
|
||||
return .iphone4
|
||||
} else if phone && maxLength == 568 {
|
||||
return .iphone5
|
||||
} else if phone && maxLength == 667 {
|
||||
return .iphone6
|
||||
} else if phone && maxLength == 736 {
|
||||
return .iphone6plus
|
||||
} else if phone && maxLength == 812 {
|
||||
return .iphoneX
|
||||
} else if pad && maxLength == 1024 {
|
||||
return .ipad9
|
||||
} else if pad && maxLength == 1112 {
|
||||
return .ipad10
|
||||
} else if pad && maxLength == 1366 {
|
||||
return .ipad12
|
||||
public static var width : CGFloat { return UIScreen.main.bounds.size.width }
|
||||
public static var height : CGFloat { return UIScreen.main.bounds.size.height }
|
||||
public static var maxLength : CGFloat { return max(width, height) }
|
||||
public static var minLength : CGFloat { return min(width, height) }
|
||||
public static var zoomed : Bool { return UIScreen.main.nativeScale >= UIScreen.main.scale }
|
||||
public static var retina : Bool { return UIScreen.main.scale >= 2.0 }
|
||||
public static var phone : Bool { return UIDevice.current.userInterfaceIdiom == .phone }
|
||||
public static var pad : Bool { return UIDevice.current.userInterfaceIdiom == .pad }
|
||||
|
||||
public static var navbarSize : CGFloat {
|
||||
return (Display.height == 812 || Display.height == 896) ? 88 : 64
|
||||
}
|
||||
public static var bottombarSize : CGFloat {
|
||||
return (Display.height == 812 || Display.height == 896) ? 34 : 0
|
||||
}
|
||||
|
||||
return .unknown
|
||||
}
|
||||
public static var displayType: DisplayType {
|
||||
if phone && maxLength < 568 {
|
||||
return .iphone4
|
||||
} else if phone && maxLength == 568 {
|
||||
return .iphone5
|
||||
} else if phone && maxLength == 667 {
|
||||
return .iphone6
|
||||
} else if phone && maxLength == 736 {
|
||||
return .iphone6plus
|
||||
} else if phone && maxLength == 896 {
|
||||
return .iphoneXR
|
||||
} else if phone && maxLength == 812 {
|
||||
return .iphoneX
|
||||
} else if pad && maxLength == 1024 {
|
||||
return .ipad9
|
||||
} else if pad && maxLength == 1112 {
|
||||
return .ipad10
|
||||
} else if pad && maxLength == 1366 {
|
||||
return .ipad12
|
||||
}
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,10 +201,10 @@ extension CATransition {
|
||||
case fade, moveIn, push, reveal
|
||||
var value: String {
|
||||
switch self {
|
||||
case .fade: return kCATransitionFade
|
||||
case .moveIn: return kCATransitionMoveIn
|
||||
case .push: return kCATransitionPush
|
||||
case .reveal: return kCATransitionReveal
|
||||
case .fade: return convertFromCATransitionType(CATransitionType.fade)
|
||||
case .moveIn: return convertFromCATransitionType(CATransitionType.moveIn)
|
||||
case .push: return convertFromCATransitionType(CATransitionType.push)
|
||||
case .reveal: return convertFromCATransitionType(CATransitionType.reveal)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,19 +213,19 @@ extension CATransition {
|
||||
case fromRight, fromLeft, fromTop, fromBottom
|
||||
var value: String {
|
||||
switch self {
|
||||
case .fromRight: return kCATransitionFromRight
|
||||
case .fromLeft: return kCATransitionFromLeft
|
||||
case .fromTop: return kCATransitionFromTop
|
||||
case .fromBottom: return kCATransitionFromBottom
|
||||
case .fromRight: return convertFromCATransitionSubtype(CATransitionSubtype.fromRight)
|
||||
case .fromLeft: return convertFromCATransitionSubtype(CATransitionSubtype.fromLeft)
|
||||
case .fromTop: return convertFromCATransitionSubtype(CATransitionSubtype.fromTop)
|
||||
case .fromBottom: return convertFromCATransitionSubtype(CATransitionSubtype.fromBottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public convenience init(type: Kind, subtype: Subkind? = nil) {
|
||||
self.init()
|
||||
self.type = type.value
|
||||
self.type = convertToCATransitionType(type.value)
|
||||
if let subtype = subtype {
|
||||
self.subtype = subtype.value
|
||||
self.subtype = convertToOptionalCATransitionSubtype(subtype.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,3 +247,24 @@ extension CATransaction {
|
||||
commit()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function inserted by Swift 4.2 migrator.
|
||||
fileprivate func convertFromCATransitionType(_ input: CATransitionType) -> String {
|
||||
return input.rawValue
|
||||
}
|
||||
|
||||
// Helper function inserted by Swift 4.2 migrator.
|
||||
fileprivate func convertFromCATransitionSubtype(_ input: CATransitionSubtype) -> String {
|
||||
return input.rawValue
|
||||
}
|
||||
|
||||
// Helper function inserted by Swift 4.2 migrator.
|
||||
fileprivate func convertToCATransitionType(_ input: String) -> CATransitionType {
|
||||
return CATransitionType(rawValue: input)
|
||||
}
|
||||
|
||||
// Helper function inserted by Swift 4.2 migrator.
|
||||
fileprivate func convertToOptionalCATransitionSubtype(_ input: String?) -> CATransitionSubtype? {
|
||||
guard let input = input else { return nil }
|
||||
return CATransitionSubtype(rawValue: input)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ extension UIAlertController {
|
||||
- returns: self
|
||||
*/
|
||||
@discardableResult
|
||||
public func addAction(title: String?, style: UIAlertActionStyle = .default, handler: ((UIAlertAction) -> Void)? = nil) -> Self {
|
||||
public func addAction(title: String?, style: UIAlertAction.Style = .default, handler: ((UIAlertAction) -> Void)? = nil) -> Self {
|
||||
addAction(UIAlertAction(title: title, style: style, handler: handler))
|
||||
return self
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ extension UIButton {
|
||||
- parameter color: The background color to use for the specified state.
|
||||
- parameter state: The state that uses the specified image.
|
||||
*/
|
||||
public func setBackgroundColor(_ color: UIColor, for state: UIControlState) {
|
||||
public func setBackgroundColor(_ color: UIColor, for state: UIControl.State) {
|
||||
setBackgroundImage(UIImage(color: color), for: state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public extension UIButton {
|
||||
/// - titlePosition: UIViewContentModeTop, UIViewContentModeBottom, UIViewContentModeLeft or UIViewContentModeRight
|
||||
/// - additionalSpacing: Spacing between image and title
|
||||
/// - state: State to apply this behaviour
|
||||
public func set(image: UIImage?, title: String, titlePosition: Position, spacing: CGFloat, state: UIControlState){
|
||||
public func set(image: UIImage?, title: String, titlePosition: Position, spacing: CGFloat, state: UIControl.State){
|
||||
imageView?.contentMode = .center
|
||||
setImage(image, for: state)
|
||||
setTitle(title, for: state)
|
||||
@@ -39,7 +39,7 @@ public extension UIButton {
|
||||
/// - titlePosition: UIViewContentModeTop, UIViewContentModeBottom, UIViewContentModeLeft or UIViewContentModeRight
|
||||
/// - additionalSpacing: Spacing between image and title
|
||||
/// - state: State to apply this behaviour
|
||||
public func set(image: UIImage?, attributedTitle title: NSAttributedString, titlePosition: Position, spacing: CGFloat, state: UIControlState){
|
||||
public func set(image: UIImage?, attributedTitle title: NSAttributedString, titlePosition: Position, spacing: CGFloat, state: UIControl.State){
|
||||
imageView?.contentMode = .center
|
||||
setImage(image, for: state)
|
||||
|
||||
@@ -57,7 +57,7 @@ public extension UIButton {
|
||||
|
||||
// Use predefined font, otherwise use the default
|
||||
let titleFont: UIFont = titleLabel?.font ?? UIFont()
|
||||
let titleSize: CGSize = title.size(withAttributes: [NSAttributedStringKey.font: titleFont])
|
||||
let titleSize: CGSize = title.size(withAttributes: [NSAttributedString.Key.font: titleFont])
|
||||
|
||||
arrange(titleSize: titleSize, imageRect: imageRect, atPosition: position, spacing: spacing)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public extension UICollectionView {
|
||||
_ type: T.Type,
|
||||
withIdentifier reuseIdentifier: String = T.reuseIdentifier)
|
||||
{
|
||||
registerSupplement(T.self, kind: UICollectionElementKindSectionHeader)
|
||||
registerSupplement(T.self, kind: UICollectionView.elementKindSectionHeader)
|
||||
}
|
||||
|
||||
func dequeueHeader<T: UICollectionReusableView>(
|
||||
@@ -52,14 +52,14 @@ public extension UICollectionView {
|
||||
withIdentifier reuseIdentifier: String = T.reuseIdentifier,
|
||||
for indexPath: IndexPath) -> T
|
||||
{
|
||||
return dequeueSupplement(kind: UICollectionElementKindSectionHeader, for: indexPath)
|
||||
return dequeueSupplement(kind: UICollectionView.elementKindSectionHeader, for: indexPath)
|
||||
}
|
||||
|
||||
func registerFooter<T: UICollectionReusableView>(
|
||||
_ type: T.Type,
|
||||
withIdentifier reuseIdentifier: String = T.reuseIdentifier)
|
||||
{
|
||||
registerSupplement(T.self, kind: UICollectionElementKindSectionFooter)
|
||||
registerSupplement(T.self, kind: UICollectionView.elementKindSectionFooter)
|
||||
}
|
||||
|
||||
func dequeueFooter<T: UICollectionReusableView>(
|
||||
@@ -67,7 +67,7 @@ public extension UICollectionView {
|
||||
withIdentifier reuseIdentifier: String = T.reuseIdentifier,
|
||||
for indexPath: IndexPath) -> T
|
||||
{
|
||||
return dequeueSupplement(kind: UICollectionElementKindSectionFooter, for: indexPath)
|
||||
return dequeueSupplement(kind: UICollectionView.elementKindSectionFooter, for: indexPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ extension UIView {
|
||||
case tap
|
||||
case longPress
|
||||
case pan
|
||||
case swipe(UISwipeGestureRecognizerDirection)
|
||||
case swipe(UISwipeGestureRecognizer.Direction)
|
||||
case tapCount(Int)
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ extension UIImage {
|
||||
- parameter orientiation: The orientation to use for the scaled image (optional, defaults to the image's `imageOrientation` property).
|
||||
- returns: A copy of self, scaled by the scaleFactor (with an optional image orientation).
|
||||
*/
|
||||
public func scaled(by scaleFactor: CGFloat, withOrientation orientation: UIImageOrientation? = nil) -> UIImage? {
|
||||
public func scaled(by scaleFactor: CGFloat, withOrientation orientation: UIImage.Orientation? = nil) -> UIImage? {
|
||||
guard let coreImage = cgImage else { return nil }
|
||||
|
||||
return UIImage(cgImage: coreImage, scale: 1/scaleFactor, orientation: orientation ?? imageOrientation)
|
||||
|
||||
@@ -4,11 +4,8 @@ extension UIScrollView {
|
||||
|
||||
func scrollToBottom() {
|
||||
layoutIfNeeded()
|
||||
|
||||
let minimumYOffset = -max(Display.navbarSize, contentInset.top)
|
||||
|
||||
contentOffset = CGPoint(x: 0,
|
||||
y: max(minimumYOffset, bounds.minY + contentSize.height + contentInset.top - bounds.height))
|
||||
let y = max(minimumYOffset, bounds.minY + contentSize.height + contentInset.top - bounds.height)
|
||||
contentOffset = CGPoint(x: 0, y: y)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -56,9 +56,9 @@ extension UIViewController {
|
||||
|
||||
extension UIViewController {
|
||||
public func add(_ child: UIViewController) {
|
||||
addChildViewController(child)
|
||||
addChild(child)
|
||||
view.addSubview(child.view)
|
||||
child.didMove(toParentViewController: self)
|
||||
child.didMove(toParent: self)
|
||||
}
|
||||
|
||||
public func remove() {
|
||||
@@ -66,8 +66,8 @@ extension UIViewController {
|
||||
return
|
||||
}
|
||||
|
||||
willMove(toParentViewController: nil)
|
||||
removeFromParentViewController()
|
||||
willMove(toParent: nil)
|
||||
removeFromParent()
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ extension UIWindow {
|
||||
//mpdal controller have strong reference to it presenting controller, to correct memory management we must dismiss it before replace parent controller.
|
||||
previous?.dismiss(animated: false, completion: nil)
|
||||
previous?.view.removeFromSuperview()
|
||||
previous?.removeFromParentViewController()
|
||||
previous?.removeFromParent()
|
||||
}
|
||||
|
||||
guard let snapShot: UIView = subviews.last?.snapshotView() else {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
class GestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate {
|
||||
var recognizerShouldBegin: ((UIGestureRecognizer) -> Bool)? = nil
|
||||
var shouldRecognizeSimultaneously: ((UIGestureRecognizer, UIGestureRecognizer) -> Bool)? = nil
|
||||
var shouldRequireFailureOf: ((UIGestureRecognizer, UIGestureRecognizer) -> Bool)? = nil
|
||||
var shouldRequireFailureBy: ((UIGestureRecognizer, UIGestureRecognizer) -> Bool)? = nil
|
||||
var shouldReceiveTouch: ((UIGestureRecognizer, UITouch) -> Bool)? = nil
|
||||
var shouldReceivePress: ((UIGestureRecognizer, UIPress) -> Bool)? = nil
|
||||
|
||||
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return recognizerShouldBegin?(gestureRecognizer) ?? true
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return shouldRecognizeSimultaneously?(gestureRecognizer, otherGestureRecognizer) ?? true
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return shouldRequireFailureOf?(gestureRecognizer, otherGestureRecognizer) ?? true
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return shouldRequireFailureBy?(gestureRecognizer, otherGestureRecognizer) ?? false
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||
return shouldReceiveTouch?(gestureRecognizer, touch) ?? true
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive press: UIPress) -> Bool {
|
||||
return shouldReceivePress?(gestureRecognizer, press) ?? true
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -32,22 +32,22 @@ public class InputVisibilityController: NSObject {
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(keyboardWillAppear),
|
||||
name: .UIKeyboardWillShow,
|
||||
name: UIResponder.keyboardWillShowNotification,
|
||||
object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(keyboardWillDisappear),
|
||||
name: .UIKeyboardWillHide,
|
||||
name: UIResponder.keyboardWillHideNotification,
|
||||
object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(applicationWillResign),
|
||||
name: .UIApplicationWillResignActive,
|
||||
name: UIApplication.willResignActiveNotification,
|
||||
object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(keyboardWillChangeFrame),
|
||||
name: .UIKeyboardWillChangeFrame,
|
||||
name: UIResponder.keyboardWillChangeFrameNotification,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ public class InputVisibilityController: NSObject {
|
||||
}
|
||||
|
||||
@objc func keyboardWillChangeFrame(notification: Notification) {
|
||||
let keyboardEndFrame = (notification.userInfo![UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
|
||||
let keyboardEndFrame = (notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
|
||||
let goingUp = (keyboardEndFrame?.origin.y ?? 0) < UIScreen.main.bounds.height
|
||||
self.moveViewUp(up: goingUp, usingKeyboardNotification: notification)
|
||||
}
|
||||
@@ -92,7 +92,7 @@ public class InputVisibilityController: NSObject {
|
||||
}
|
||||
|
||||
let userInfo = notification.userInfo!
|
||||
let keyboardEndFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
|
||||
let keyboardEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
|
||||
|
||||
var toBeVisibleView = self.toBeVisibleView
|
||||
|
||||
@@ -106,11 +106,11 @@ public class InputVisibilityController: NSObject {
|
||||
// the old way of animation will match the keyboard animation timing and curve
|
||||
if !self.disableKeyboardMoveUpAnimation {
|
||||
UIView.beginAnimations(nil, context: nil)
|
||||
if let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? Double {
|
||||
if let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double {
|
||||
UIView.setAnimationDuration(duration)
|
||||
}
|
||||
if let animationValue = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? Int {
|
||||
if let animationCurve = UIViewAnimationCurve(rawValue: animationValue) {
|
||||
if let animationValue = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int {
|
||||
if let animationCurve = UIView.AnimationCurve(rawValue: animationValue) {
|
||||
UIView.setAnimationCurve(animationCurve)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,28 +18,28 @@ public enum KeyboardEventType {
|
||||
|
||||
public var notificationName: NSNotification.Name {
|
||||
switch self {
|
||||
case .willShow: return .UIKeyboardWillShow
|
||||
case .didShow: return .UIKeyboardDidShow
|
||||
case .willHide: return .UIKeyboardWillHide
|
||||
case .didHide: return .UIKeyboardDidHide
|
||||
case .willChangeFrame: return .UIKeyboardWillChangeFrame
|
||||
case .didChangeFrame: return .UIKeyboardDidChangeFrame
|
||||
case .willShow: return UIResponder.keyboardWillShowNotification
|
||||
case .didShow: return UIResponder.keyboardDidShowNotification
|
||||
case .willHide: return UIResponder.keyboardWillHideNotification
|
||||
case .didHide: return UIResponder.keyboardDidHideNotification
|
||||
case .willChangeFrame: return UIResponder.keyboardWillChangeFrameNotification
|
||||
case .didChangeFrame: return UIResponder.keyboardDidChangeFrameNotification
|
||||
}
|
||||
}
|
||||
|
||||
init?(name: NSNotification.Name) {
|
||||
switch name {
|
||||
case NSNotification.Name.UIKeyboardWillShow:
|
||||
case UIResponder.keyboardWillShowNotification:
|
||||
self = .willShow
|
||||
case NSNotification.Name.UIKeyboardDidShow:
|
||||
case UIResponder.keyboardDidShowNotification:
|
||||
self = .didShow
|
||||
case NSNotification.Name.UIKeyboardWillHide:
|
||||
case UIResponder.keyboardWillHideNotification:
|
||||
self = .willHide
|
||||
case NSNotification.Name.UIKeyboardDidHide:
|
||||
case UIResponder.keyboardDidHideNotification:
|
||||
self = .didHide
|
||||
case NSNotification.Name.UIKeyboardWillChangeFrame:
|
||||
case UIResponder.keyboardWillChangeFrameNotification:
|
||||
self = .willChangeFrame
|
||||
case NSNotification.Name.UIKeyboardDidChangeFrame:
|
||||
case UIResponder.keyboardDidChangeFrameNotification:
|
||||
self = .didChangeFrame
|
||||
default:
|
||||
return nil
|
||||
@@ -62,27 +62,27 @@ public struct KeyboardEvent {
|
||||
public let type: KeyboardEventType
|
||||
public let keyboardFrameBegin: CGRect
|
||||
public let keyboardFrameEnd: CGRect
|
||||
public let curve: UIViewAnimationCurve
|
||||
public let curve: UIView.AnimationCurve
|
||||
public let duration: TimeInterval
|
||||
public let isLocal: Bool
|
||||
|
||||
public var options: UIViewAnimationOptions {
|
||||
return UIViewAnimationOptions(rawValue: UInt(curve.rawValue << 16))
|
||||
public var options: UIView.AnimationOptions {
|
||||
return UIView.AnimationOptions(rawValue: UInt(curve.rawValue << 16))
|
||||
}
|
||||
|
||||
init?(notification: Notification) {
|
||||
guard let userInfo = (notification as NSNotification).userInfo else { return nil }
|
||||
guard let type = KeyboardEventType(name: notification.name) else { return nil }
|
||||
guard let begin = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue else { return nil }
|
||||
guard let end = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return nil }
|
||||
guard let begin = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue else { return nil }
|
||||
guard let end = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return nil }
|
||||
guard
|
||||
let curveInt = (userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber)?.intValue,
|
||||
let curve = UIViewAnimationCurve(rawValue: curveInt)
|
||||
let curveInt = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.intValue,
|
||||
let curve = UIView.AnimationCurve(rawValue: curveInt)
|
||||
else { return nil }
|
||||
guard
|
||||
let durationDouble = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue
|
||||
let durationDouble = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue
|
||||
else { return nil }
|
||||
guard let isLocalInt = (userInfo[UIKeyboardIsLocalUserInfoKey] as? NSNumber)?.intValue
|
||||
guard let isLocalInt = (userInfo[UIResponder.keyboardIsLocalUserInfoKey] as? NSNumber)?.intValue
|
||||
else { return nil }
|
||||
|
||||
self.type = type
|
||||
|
||||
@@ -11,11 +11,13 @@ extension UIControl: Togglable {
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
extension UISwitch {
|
||||
public override func selectedToggle(select: Bool) {
|
||||
setOn(select, animated: true)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public struct Toggler {
|
||||
var togglers = [Togglable]()
|
||||
@@ -62,10 +64,10 @@ public class ButtonsTogglerView: UIStackView {
|
||||
|
||||
public required init(defaultIndex index: Int = 0,
|
||||
buttons: [UIButton],
|
||||
axis: UILayoutConstraintAxis = .horizontal,
|
||||
axis: NSLayoutConstraint.Axis = .horizontal,
|
||||
spacing: CGFloat = 30,
|
||||
distribution: UIStackViewDistribution = .equalSpacing,
|
||||
alignment: UIStackViewAlignment = .fill) {
|
||||
distribution: UIStackView.Distribution = .equalSpacing,
|
||||
alignment: UIStackView.Alignment = .fill) {
|
||||
|
||||
currentIndex = index
|
||||
toggler = Toggler(default: index, togglers: buttons)
|
||||
|
||||
@@ -3,134 +3,139 @@ import UIKit
|
||||
extension UIViewController {
|
||||
|
||||
public func findUp<T>() -> T? where T: UIViewController {
|
||||
return findUpVC(self)
|
||||
return UISearchUtilities.findUpVC(self)
|
||||
}
|
||||
|
||||
public func findDown<T>() -> T? where T: UIViewController {
|
||||
return findDownVC(self)
|
||||
return UISearchUtilities.findDownVC(self)
|
||||
}
|
||||
}
|
||||
|
||||
func findUpVC<T>(_ base: UIViewController) -> T? where T: UIViewController {
|
||||
|
||||
var vc: UIViewController? = base
|
||||
|
||||
while vc != nil {
|
||||
if let vc = vc?.parent as? T {
|
||||
return vc
|
||||
} else {
|
||||
vc = vc?.parent
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findDownVC<T>(_ base: UIViewController) -> T? where T: UIViewController {
|
||||
var result: T?
|
||||
for c in base.childViewControllers {
|
||||
if let r = c as? T {
|
||||
result = r
|
||||
} else {
|
||||
result = findDownVC(c)
|
||||
struct UISearchUtilities {
|
||||
|
||||
static func findUpVC<T>(_ base: UIViewController) -> T? where T: UIViewController {
|
||||
|
||||
var vc: UIViewController? = base
|
||||
|
||||
while vc != nil {
|
||||
if let vc = vc?.parent as? T {
|
||||
return vc
|
||||
} else {
|
||||
vc = vc?.parent
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if result != nil {
|
||||
break
|
||||
|
||||
static func findDownVC<T>(_ base: UIViewController) -> T? where T: UIViewController {
|
||||
var result: T?
|
||||
for c in base.children {
|
||||
if let r = c as? T {
|
||||
result = r
|
||||
} else {
|
||||
result = findDownVC(c)
|
||||
}
|
||||
if result != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static func findDownVC<T>() -> T? where T: UIViewController {
|
||||
guard let base = UIApplication.shared.keyWindow?.rootViewController else { return nil }
|
||||
if let result = base as? T {
|
||||
return result
|
||||
}
|
||||
return findDownVC(base)
|
||||
}
|
||||
|
||||
static func first(inView view: UIView, where condition: @escaping (UIView) -> Bool) -> UIView? {
|
||||
guard !condition(view) else { return view }
|
||||
|
||||
var result: UIView?
|
||||
for v in view.subviews {
|
||||
if condition(v) {
|
||||
result = v
|
||||
} else {
|
||||
result = first(inView: v, where: condition)
|
||||
}
|
||||
if result != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static func findView<T>(_ root: UIView) -> T? where T: UIView {
|
||||
var result: T?
|
||||
for v in root.subviews {
|
||||
if let r = v as? T {
|
||||
result = r
|
||||
} else {
|
||||
result = findView(v)
|
||||
}
|
||||
if result != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static func findSuperView<T>(_ base: UIView) -> T? where T: UIView {
|
||||
var v: UIView? = base
|
||||
while v != nil {
|
||||
if let v = v?.superview as? T {
|
||||
return v
|
||||
} else {
|
||||
v = v?.superview
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func findAllViews<T>(_ root: UIView) -> [T] where T: UIView {
|
||||
var result: [T] = []
|
||||
for v in root.subviews {
|
||||
if let r = v as? T {
|
||||
result.append(r)
|
||||
}
|
||||
let rv: [T] = findAllViews(v)
|
||||
result.append(contentsOf: rv)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func findDownVC<T>() -> T? where T: UIViewController {
|
||||
guard let base = UIApplication.shared.keyWindow?.rootViewController else { return nil }
|
||||
if let result = base as? T {
|
||||
return result
|
||||
}
|
||||
return findDownVC(base)
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
|
||||
public func first(where condition: @escaping (UIView) -> Bool) -> UIView? {
|
||||
return Utopia.first(inView: self, where: condition)
|
||||
return UISearchUtilities.first(inView: self, where: condition)
|
||||
}
|
||||
|
||||
public func find<T>() -> T? where T: UIView {
|
||||
return Utopia.findView(self)
|
||||
return UISearchUtilities.findView(self)
|
||||
}
|
||||
|
||||
public func findSuperView<T>() -> T? where T: UIView {
|
||||
return Utopia.findSuperView(self)
|
||||
return UISearchUtilities.findSuperView(self)
|
||||
}
|
||||
|
||||
public func findAll<T>() -> [T] where T: UIView {
|
||||
return findAllViews(self)
|
||||
return UISearchUtilities.findAllViews(self)
|
||||
}
|
||||
}
|
||||
|
||||
func first(inView view: UIView, where condition: @escaping (UIView) -> Bool) -> UIView? {
|
||||
guard !condition(view) else { return view }
|
||||
|
||||
var result: UIView?
|
||||
for v in view.subviews {
|
||||
if condition(v) {
|
||||
result = v
|
||||
} else {
|
||||
result = first(inView: v, where: condition)
|
||||
}
|
||||
if result != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func findView<T>(_ root: UIView) -> T? where T: UIView {
|
||||
var result: T?
|
||||
for v in root.subviews {
|
||||
if let r = v as? T {
|
||||
result = r
|
||||
} else {
|
||||
result = findView(v)
|
||||
}
|
||||
if result != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func findSuperView<T>(_ base: UIView) -> T? where T: UIView {
|
||||
var v: UIView? = base
|
||||
while v != nil {
|
||||
if let v = v?.superview as? T {
|
||||
return v
|
||||
} else {
|
||||
v = v?.superview
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findAllViews<T>(_ root: UIView) -> [T] where T: UIView {
|
||||
var result: [T] = []
|
||||
for v in root.subviews {
|
||||
if let r = v as? T {
|
||||
result.append(r)
|
||||
}
|
||||
let rv: [T] = findAllViews(v)
|
||||
result.append(contentsOf: rv)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
|
||||
public func findConstraints(attribute: NSLayoutAttribute) -> [NSLayoutConstraint] {
|
||||
public func findConstraints(attribute: NSLayoutConstraint.Attribute) -> [NSLayoutConstraint] {
|
||||
let result = constraints.filter { $0.firstAttribute == attribute && $0.firstItem as? NSObject == self }
|
||||
return result
|
||||
}
|
||||
|
||||
public func findSuperviewConstraints(attribute: NSLayoutAttribute) -> [NSLayoutConstraint] {
|
||||
public func findSuperviewConstraints(attribute: NSLayoutConstraint.Attribute) -> [NSLayoutConstraint] {
|
||||
let result = superview?.constraints.filter {
|
||||
($0.firstAttribute == attribute && $0.firstItem as? NSObject == self) ||
|
||||
($0.secondAttribute == attribute && $0.secondItem as? NSObject == self)
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
//
|
||||
// DiffAware.swift
|
||||
// DeepDiff
|
||||
//
|
||||
// Created by Khoa Pham on 03.01.2018.
|
||||
// Copyright © 2018 Khoa Pham. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol DiffAware {
|
||||
func diff<T: Hashable>(old: Array<T>, new: Array<T>) -> [Change<T>]
|
||||
}
|
||||
|
||||
extension DiffAware {
|
||||
func preprocess<T: Hashable>(old: Array<T>, new: Array<T>) -> [Change<T>]? {
|
||||
switch (old.isEmpty, new.isEmpty) {
|
||||
case (true, true):
|
||||
// empty
|
||||
return []
|
||||
case (true, false):
|
||||
// all .insert
|
||||
return new.enumerated().map { index, item in
|
||||
return .insert(Insert(item: item, index: index))
|
||||
}
|
||||
case (false, true):
|
||||
// all .delete
|
||||
return old.enumerated().map { index, item in
|
||||
return .delete(Delete(item: item, index: index))
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
// https://gist.github.com/ndarville/3166060
|
||||
|
||||
public final class Heckel: DiffAware {
|
||||
|
||||
// OC and NC can assume three values: 1, 2, and many.
|
||||
enum Counter {
|
||||
case zero, one, many
|
||||
|
||||
func increment() -> Counter {
|
||||
switch self {
|
||||
case .zero:
|
||||
return .one
|
||||
case .one:
|
||||
return .many
|
||||
case .many:
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The symbol table stores three entries for each line
|
||||
class TableEntry: Equatable {
|
||||
// The value entry for each line in table has two counters.
|
||||
// They specify the line's number of occurrences in O and N: OC and NC.
|
||||
var oldCounter: Counter = .zero
|
||||
var newCounter: Counter = .zero
|
||||
|
||||
// Aside from the two counters, the line's entry
|
||||
// also includes a reference to the line's line number in O: OLNO.
|
||||
// OLNO is only interesting, if OC == 1.
|
||||
// Alternatively, OLNO would have to assume multiple values or none at all.
|
||||
var indexesInOld: [Int] = []
|
||||
|
||||
static func ==(lhs: TableEntry, rhs: TableEntry) -> Bool {
|
||||
return lhs.oldCounter == rhs.oldCounter && lhs.newCounter == rhs.newCounter && lhs.indexesInOld == rhs.indexesInOld
|
||||
}
|
||||
}
|
||||
|
||||
// The arrays OA and NA have one entry for each line in their respective files, O and N.
|
||||
// The arrays contain either:
|
||||
enum ArrayEntry: Equatable {
|
||||
// a pointer to the line's symbol table entry, table[line]
|
||||
case tableEntry(TableEntry)
|
||||
|
||||
// the line's number in the other file (N for OA, O for NA)
|
||||
case indexInOther(Int)
|
||||
|
||||
public static func == (lhs: ArrayEntry, rhs: ArrayEntry) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.tableEntry(let l), .tableEntry(let r)):
|
||||
return l == r
|
||||
case (.indexInOther(let l), .indexInOther(let r)):
|
||||
return l == r
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public func diff<T: Hashable>(old: Array<T>, new: Array<T>) -> [Change<T>] {
|
||||
// The Symbol Table
|
||||
// Each line works as the key in the table look-up, i.e. as table[line].
|
||||
var table: [Int: TableEntry] = [:]
|
||||
|
||||
// The arrays OA and NA have one entry for each line in their respective files, O and N
|
||||
var oldArray = [ArrayEntry]()
|
||||
var newArray = [ArrayEntry]()
|
||||
|
||||
perform1stPass(new: new, table: &table, newArray: &newArray)
|
||||
perform2ndPass(old: old, table: &table, oldArray: &oldArray)
|
||||
perform345Pass(newArray: &newArray, oldArray: &oldArray)
|
||||
let changes = perform6thPass(new: new, old: old, newArray: newArray, oldArray: oldArray)
|
||||
return changes
|
||||
}
|
||||
|
||||
private func perform1stPass<T: Hashable>(
|
||||
new: Array<T>,
|
||||
table: inout [Int: TableEntry],
|
||||
newArray: inout [ArrayEntry]) {
|
||||
|
||||
// 1st pass
|
||||
// a. Each line i of file N is read in sequence
|
||||
new.forEach { item in
|
||||
// b. An entry for each line i is created in the table, if it doesn't already exist
|
||||
let entry = table[item.hashValue] ?? TableEntry()
|
||||
|
||||
// c. NC for the line's table entry is incremented
|
||||
entry.newCounter = entry.newCounter.increment()
|
||||
|
||||
// d. NA[i] is set to point to the table entry of line i
|
||||
newArray.append(.tableEntry(entry))
|
||||
|
||||
//
|
||||
table[item.hashValue] = entry
|
||||
}
|
||||
}
|
||||
|
||||
private func perform2ndPass<T: Hashable>(
|
||||
old: Array<T>,
|
||||
table: inout [Int: TableEntry],
|
||||
oldArray: inout [ArrayEntry]) {
|
||||
|
||||
// 2nd pass
|
||||
// Similar to first pass, except it acts on files
|
||||
|
||||
old.enumerated().forEach { tuple in
|
||||
// old
|
||||
let entry = table[tuple.element.hashValue] ?? TableEntry()
|
||||
|
||||
// oldCounter
|
||||
entry.oldCounter = entry.oldCounter.increment()
|
||||
|
||||
// lineNumberInOld which is set to the line's number
|
||||
entry.indexesInOld.append(tuple.offset)
|
||||
|
||||
// oldArray
|
||||
oldArray.append(.tableEntry(entry))
|
||||
|
||||
//
|
||||
table[tuple.element.hashValue] = entry
|
||||
}
|
||||
}
|
||||
|
||||
private func perform345Pass(newArray: inout [ArrayEntry], oldArray: inout [ArrayEntry]) {
|
||||
// 3rd pass
|
||||
// a. We use Observation 1:
|
||||
// If a line occurs only once in each file, then it must be the same line,
|
||||
// although it may have been moved.
|
||||
// We use this observation to locate unaltered lines that we
|
||||
// subsequently exclude from further treatment.
|
||||
// b. Using this, we only process the lines where OC == NC == 1
|
||||
// c. As the lines between O and N "must be the same line,
|
||||
// although it may have been moved", we alter the table pointers
|
||||
// in OA and NA to the number of the line in the other file.
|
||||
// d. We also locate unique virtual lines
|
||||
// immediately before the first and
|
||||
// immediately after the last lines of the files ???
|
||||
//
|
||||
// 4th pass
|
||||
// a. We use Observation 2:
|
||||
// If a line has been found to be unaltered,
|
||||
// and the lines immediately adjacent to it in both files are identical,
|
||||
// then these lines must be the same line.
|
||||
// This information can be used to find blocks of unchanged lines.
|
||||
// b. Using this, we process each entry in ascending order.
|
||||
// c. If
|
||||
// NA[i] points to OA[j], and
|
||||
// NA[i+1] and OA[j+1] contain identical table entry pointers
|
||||
// then
|
||||
// OA[j+1] is set to line i+1, and
|
||||
// NA[i+1] is set to line j+1
|
||||
//
|
||||
// 5th pass
|
||||
// Similar to fourth pass, except:
|
||||
// It processes each entry in descending order
|
||||
// It uses j-1 and i-1 instead of j+1 and i+1
|
||||
|
||||
newArray.enumerated().forEach { (indexOfNew, item) in
|
||||
switch item {
|
||||
case .tableEntry(let entry):
|
||||
guard !entry.indexesInOld.isEmpty else {
|
||||
return
|
||||
}
|
||||
let indexOfOld = entry.indexesInOld.removeFirst()
|
||||
let isObservation1 = entry.newCounter == .one && entry.oldCounter == .one
|
||||
let isObservation2 = entry.newCounter != .zero && entry.oldCounter != .zero && newArray[indexOfNew] == oldArray[indexOfOld]
|
||||
guard isObservation1 || isObservation2 else {
|
||||
return
|
||||
}
|
||||
newArray[indexOfNew] = .indexInOther(indexOfOld)
|
||||
oldArray[indexOfOld] = .indexInOther(indexOfNew)
|
||||
case .indexInOther(_):
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func perform6thPass<T: Hashable>(
|
||||
new: Array<T>,
|
||||
old: Array<T>,
|
||||
newArray: [ArrayEntry],
|
||||
oldArray: [ArrayEntry]) -> [Change<T>] {
|
||||
|
||||
// 6th pass
|
||||
// At this point following our five passes,
|
||||
// we have the necessary information contained in NA to tell the differences between O and N.
|
||||
// This pass uses NA and OA to tell when a line has changed between O and N,
|
||||
// and how far the change extends.
|
||||
|
||||
// a. Determining a New Line
|
||||
// Recall our initial description of NA in which we said that the array has either:
|
||||
// one entry for each line of file N containing either
|
||||
// a pointer to table[line]
|
||||
// the line's number in file O
|
||||
|
||||
// Using these two cases, we know that if NA[i] refers
|
||||
// to an entry in table (case 1), then line i must be new
|
||||
// We know this, because otherwise, NA[i] would have contained
|
||||
// the line's number in O (case 2), if it existed in O and N
|
||||
|
||||
// b. Determining the Boundaries of the New Line
|
||||
// We now know that we are dealing with a new line, but we have yet to figure where the change ends.
|
||||
// Recall Observation 2:
|
||||
|
||||
// If NA[i] points to OA[j], but NA[i+1] does not
|
||||
// point to OA[j+1], then line i is the boundary for the alteration.
|
||||
|
||||
// You can look at it this way:
|
||||
// i : The quick brown fox | j : The quick brown fox
|
||||
// i+1: jumps over the lazy dog | j+1: jumps over the loafing cat
|
||||
|
||||
// Here, NA[i] == OA[j], but NA[i+1] != OA[j+1].
|
||||
// This means our boundary is between the two lines.
|
||||
|
||||
var changes = [Change<T>]()
|
||||
var deleteOffsets = Array(repeating: 0, count: old.count)
|
||||
|
||||
// deletions
|
||||
do {
|
||||
var runningOffset = 0
|
||||
|
||||
oldArray.enumerated().forEach { oldTuple in
|
||||
deleteOffsets[oldTuple.offset] = runningOffset
|
||||
|
||||
guard case .tableEntry = oldTuple.element else {
|
||||
return
|
||||
}
|
||||
|
||||
changes.append(.delete(Delete(
|
||||
item: old[oldTuple.offset],
|
||||
index: oldTuple.offset
|
||||
)))
|
||||
|
||||
runningOffset += 1
|
||||
}
|
||||
}
|
||||
|
||||
// insertions, replaces, moves
|
||||
do {
|
||||
var runningOffset = 0
|
||||
|
||||
newArray.enumerated().forEach { newTuple in
|
||||
switch newTuple.element {
|
||||
case .tableEntry:
|
||||
runningOffset += 1
|
||||
changes.append(.insert(Insert(
|
||||
item: new[newTuple.offset],
|
||||
index: newTuple.offset
|
||||
)))
|
||||
case .indexInOther(let oldIndex):
|
||||
if old[oldIndex] != new[newTuple.offset] {
|
||||
changes.append(.replace(Replace(
|
||||
oldItem: old[oldIndex],
|
||||
newItem: new[newTuple.offset],
|
||||
index: newTuple.offset
|
||||
)))
|
||||
}
|
||||
|
||||
let deleteOffset = deleteOffsets[oldIndex]
|
||||
// The object is not at the expected position, so move it.
|
||||
if (oldIndex - deleteOffset + runningOffset) != newTuple.offset {
|
||||
changes.append(.move(Move(
|
||||
item: new[newTuple.offset],
|
||||
fromIndex: oldIndex,
|
||||
toIndex: newTuple.offset
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
// https://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm
|
||||
|
||||
public final class WagnerFischer: DiffAware {
|
||||
private let reduceMove: Bool
|
||||
|
||||
public init(reduceMove: Bool = false) {
|
||||
self.reduceMove = reduceMove
|
||||
}
|
||||
|
||||
public func diff<T: Hashable>(old: Array<T>, new: Array<T>) -> [Change<T>] {
|
||||
let previousRow = Row<T>()
|
||||
previousRow.seed(with: new)
|
||||
let currentRow = Row<T>()
|
||||
|
||||
// row in matrix
|
||||
old.enumerated().forEach { indexInOld, oldItem in
|
||||
// reset current row
|
||||
currentRow.reset(
|
||||
count: previousRow.slots.count,
|
||||
indexInOld: indexInOld,
|
||||
oldItem: oldItem
|
||||
)
|
||||
|
||||
// column in matrix
|
||||
new.enumerated().forEach { indexInNew, newItem in
|
||||
if isEqual(oldItem: old[indexInOld], newItem: new[indexInNew]) {
|
||||
currentRow.update(indexInNew: indexInNew, previousRow: previousRow)
|
||||
} else {
|
||||
currentRow.updateWithMin(
|
||||
previousRow: previousRow,
|
||||
indexInNew: indexInNew,
|
||||
newItem: newItem,
|
||||
indexInOld: indexInOld,
|
||||
oldItem: oldItem
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// set previousRow
|
||||
previousRow.slots = currentRow.slots
|
||||
}
|
||||
|
||||
let changes = currentRow.lastSlot()
|
||||
if reduceMove {
|
||||
return MoveReducer<T>().reduce(changes: changes)
|
||||
} else {
|
||||
return changes
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper
|
||||
|
||||
private func isEqual<T: Hashable>(oldItem: T, newItem: T) -> Bool {
|
||||
// Same items must have same hashValue
|
||||
if oldItem.hashValue != newItem.hashValue {
|
||||
return false
|
||||
} else {
|
||||
// Different hashValue does not always mean different items
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We can adapt the algorithm to use less space, O(m) instead of O(mn),
|
||||
// since it only requires that the previous row and current row be stored at any one time
|
||||
class Row<T> {
|
||||
/// Each slot is a collection of Change
|
||||
var slots: [[Change<T>]] = []
|
||||
|
||||
/// Seed with .insert from new
|
||||
func seed(with new: Array<T>) {
|
||||
// First slot should be empty
|
||||
slots = Array(repeatElement([], count: new.count + 1))
|
||||
|
||||
// Each slot increases in the number of changes
|
||||
new.enumerated().forEach { index, item in
|
||||
let slotIndex = convert(indexInNew: index)
|
||||
slots[slotIndex] = combine(
|
||||
slot: slots[slotIndex-1],
|
||||
change: .insert(Insert(item: item, index: index))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset with empty slots
|
||||
/// First slot is .delete
|
||||
func reset(count: Int, indexInOld: Int, oldItem: T) {
|
||||
if slots.isEmpty {
|
||||
slots = Array(repeatElement([], count: count))
|
||||
}
|
||||
|
||||
slots[0] = combine(
|
||||
slot: slots[0],
|
||||
change: .delete(Delete(item: oldItem, index: indexInOld))
|
||||
)
|
||||
}
|
||||
|
||||
/// Use .replace from previousRow
|
||||
func update(indexInNew: Int, previousRow: Row) {
|
||||
let slotIndex = convert(indexInNew: indexInNew)
|
||||
slots[slotIndex] = previousRow.slots[slotIndex - 1]
|
||||
}
|
||||
|
||||
/// Choose the min
|
||||
func updateWithMin(previousRow: Row, indexInNew: Int, newItem: T, indexInOld: Int, oldItem: T) {
|
||||
let slotIndex = convert(indexInNew: indexInNew)
|
||||
let topSlot = previousRow.slots[slotIndex]
|
||||
let leftSlot = slots[slotIndex - 1]
|
||||
let topLeftSlot = previousRow.slots[slotIndex - 1]
|
||||
|
||||
let minCount = min(topSlot.count, leftSlot.count, topLeftSlot.count)
|
||||
|
||||
// Order of cases does not matter
|
||||
switch minCount {
|
||||
case topSlot.count:
|
||||
slots[slotIndex] = combine(
|
||||
slot: topSlot,
|
||||
change: .delete(Delete(item: oldItem, index: indexInOld))
|
||||
)
|
||||
case leftSlot.count:
|
||||
slots[slotIndex] = combine(
|
||||
slot: leftSlot,
|
||||
change: .insert(Insert(item: newItem, index: indexInNew))
|
||||
)
|
||||
case topLeftSlot.count:
|
||||
slots[slotIndex] = combine(
|
||||
slot: topLeftSlot,
|
||||
change: .replace(Replace(oldItem: oldItem, newItem: newItem, index: indexInNew))
|
||||
)
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
/// Add one more change
|
||||
func combine<T>(slot: [Change<T>], change: Change<T>) -> [Change<T>] {
|
||||
var slot = slot
|
||||
slot.append(change)
|
||||
return slot
|
||||
}
|
||||
|
||||
//// Last slot
|
||||
func lastSlot() -> [Change<T>] {
|
||||
return slots[slots.count - 1]
|
||||
}
|
||||
|
||||
/// Convert to slotIndex, as slots has 1 extra at the beginning
|
||||
func convert(indexInNew: Int) -> Int {
|
||||
return indexInNew + 1
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public struct Insert<T> {
|
||||
public let item: T
|
||||
public let index: Int
|
||||
}
|
||||
|
||||
public struct Delete<T> {
|
||||
public let item: T
|
||||
public let index: Int
|
||||
}
|
||||
|
||||
public struct Replace<T> {
|
||||
public let oldItem: T
|
||||
public let newItem: T
|
||||
public let index: Int
|
||||
}
|
||||
|
||||
public struct Move<T> {
|
||||
public let item: T
|
||||
public let fromIndex: Int
|
||||
public let toIndex: Int
|
||||
}
|
||||
|
||||
/// The computed changes from diff
|
||||
///
|
||||
/// - insert: Insert an item at index
|
||||
/// - delete: Delete an item from index
|
||||
/// - replace: Replace an item at index with another item
|
||||
/// - move: Move the same item from this index to another index
|
||||
public enum Change<T> {
|
||||
case insert(Insert<T>)
|
||||
case delete(Delete<T>)
|
||||
case replace(Replace<T>)
|
||||
case move(Move<T>)
|
||||
|
||||
public var insert: Insert<T>? {
|
||||
if case .insert(let insert) = self {
|
||||
return insert
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public var delete: Delete<T>? {
|
||||
if case .delete(let delete) = self {
|
||||
return delete
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public var replace: Replace<T>? {
|
||||
if case .replace(let replace) = self {
|
||||
return replace
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public var move: Move<T>? {
|
||||
if case .move(let move) = self {
|
||||
return move
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension Collection {
|
||||
func executeIfPresent(_ closure: (Self) -> Void) {
|
||||
if !isEmpty {
|
||||
closure(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == Int {
|
||||
var asIndexSet: IndexSet {
|
||||
return reduce(IndexSet()) { set, item in
|
||||
var set = set
|
||||
set.insert(item)
|
||||
return set
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// Perform diff between old and new collections
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - old: Old collection
|
||||
/// - new: New collection
|
||||
/// - Returns: A set of changes
|
||||
public func diff<T: Hashable>(
|
||||
old: Array<T>,
|
||||
new: Array<T>,
|
||||
algorithm: DiffAware = Heckel()) -> [Change<T>] {
|
||||
|
||||
if let changes = algorithm.preprocess(old: old, new: new) {
|
||||
return changes
|
||||
}
|
||||
|
||||
return algorithm.diff(old: old, new: new)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
struct MoveReducer<T> {
|
||||
func reduce<T: Equatable>(changes: [Change<T>]) -> [Change<T>] {
|
||||
// Find pairs of .insert and .delete with same item
|
||||
let inserts = changes.compactMap({ $0.insert })
|
||||
|
||||
if inserts.isEmpty {
|
||||
return changes
|
||||
}
|
||||
|
||||
var changes = changes
|
||||
inserts.forEach { insert in
|
||||
if let insertIndex = changes.index(where: { $0.insert?.item == insert.item }),
|
||||
let deleteIndex = changes.index(where: { $0.delete?.item == insert.item }) {
|
||||
|
||||
let insertChange = changes[insertIndex].insert!
|
||||
let deleteChange = changes[deleteIndex].delete!
|
||||
|
||||
let move = Move<T>(item: insert.item, fromIndex: deleteChange.index, toIndex: insertChange.index)
|
||||
|
||||
// .insert can be before or after .delete
|
||||
let minIndex = min(insertIndex, deleteIndex)
|
||||
let maxIndex = max(insertIndex, deleteIndex)
|
||||
|
||||
// remove both .insert and .delete, and replace by .move
|
||||
changes.remove(at: minIndex)
|
||||
changes.remove(at: maxIndex.advanced(by: -1))
|
||||
changes.insert(.move(move), at: minIndex)
|
||||
}
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public struct ChangeWithIndexPath {
|
||||
|
||||
public let inserts: [IndexPath]
|
||||
public let deletes: [IndexPath]
|
||||
public let replaces: [IndexPath]
|
||||
public let moves: [(from: IndexPath, to: IndexPath)]
|
||||
|
||||
public init(
|
||||
inserts: [IndexPath],
|
||||
deletes: [IndexPath],
|
||||
replaces:[IndexPath],
|
||||
moves: [(from: IndexPath, to: IndexPath)]) {
|
||||
|
||||
self.inserts = inserts
|
||||
self.deletes = deletes
|
||||
self.replaces = replaces
|
||||
self.moves = moves
|
||||
}
|
||||
}
|
||||
|
||||
public class IndexPathConverter {
|
||||
|
||||
public init() {
|
||||
|
||||
}
|
||||
|
||||
public func convert<T>(changes: [Change<T>], section: Int) -> ChangeWithIndexPath {
|
||||
let inserts = changes.compactMap({ $0.insert }).map({ $0.index.toIndexPath(section: section) })
|
||||
let deletes = changes.compactMap({ $0.delete }).map({ $0.index.toIndexPath(section: section) })
|
||||
let replaces = changes.compactMap({ $0.replace }).map({ $0.index.toIndexPath(section: section) })
|
||||
let moves = changes.compactMap({ $0.move }).map({
|
||||
(
|
||||
from: $0.fromIndex.toIndexPath(section: section),
|
||||
to: $0.toIndex.toIndexPath(section: section)
|
||||
)
|
||||
})
|
||||
|
||||
return ChangeWithIndexPath(
|
||||
inserts: inserts,
|
||||
deletes: deletes,
|
||||
replaces: replaces,
|
||||
moves: moves
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Int {
|
||||
|
||||
fileprivate func toIndexPath(section: Int) -> IndexPath {
|
||||
return IndexPath(item: self, section: section)
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public struct ChangeWithIndexSet {
|
||||
|
||||
public let inserts: IndexSet
|
||||
public let deletes: IndexSet
|
||||
public let replaces: IndexSet
|
||||
public let moves: [(from: Int, to: Int)]
|
||||
|
||||
public init(
|
||||
inserts: IndexSet,
|
||||
deletes: IndexSet,
|
||||
replaces: IndexSet,
|
||||
moves: [(from: Int, to: Int)]) {
|
||||
|
||||
self.inserts = inserts
|
||||
self.deletes = deletes
|
||||
self.replaces = replaces
|
||||
self.moves = moves
|
||||
}
|
||||
|
||||
public init<T>(changes: [Change<T>]) {
|
||||
inserts = changes
|
||||
.compactMap { $0.insert?.index }
|
||||
.asIndexSet
|
||||
|
||||
deletes = changes
|
||||
.compactMap { $0.delete?.index }
|
||||
.asIndexSet
|
||||
|
||||
replaces = changes
|
||||
.compactMap { $0.replace?.index }
|
||||
.asIndexSet
|
||||
|
||||
moves = changes
|
||||
.compactMap { $0.move }
|
||||
.map { (from: $0.fromIndex, to: $0.toIndex) }
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
public extension UICollectionView {
|
||||
|
||||
/// Animate reload in a batch update
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - changes: The changes from diff
|
||||
/// - section: The section that all calculated IndexPath belong
|
||||
/// - completion: Called when operation completes
|
||||
public func reload<T: Hashable>(
|
||||
changes: [Change<T>],
|
||||
section: Int = 0,
|
||||
calledInsideBatch: Bool = false,
|
||||
completion: @escaping (Bool) -> Void = { _ in }) {
|
||||
|
||||
let changesWithIndexPath = IndexPathConverter().convert(changes: changes, section: section)
|
||||
|
||||
if calledInsideBatch {
|
||||
internalBatchUpdates(changesWithIndexPath: changesWithIndexPath)
|
||||
} else {
|
||||
performBatchUpdates({
|
||||
internalBatchUpdates(changesWithIndexPath: changesWithIndexPath)
|
||||
}, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Animate sections reload in a batch update
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - changes: The changes from diff
|
||||
/// - completion: Called when operation completes
|
||||
public func reloadSections<T: Hashable>(
|
||||
changes: [Change<T>],
|
||||
calledInsideBatch: Bool = false,
|
||||
completion: @escaping (Bool) -> Void = { _ in }) {
|
||||
|
||||
let changesWithIndexSet = ChangeWithIndexSet(changes: changes)
|
||||
|
||||
if calledInsideBatch {
|
||||
internalBatchUpdates(changesWithIndexSet: changesWithIndexSet)
|
||||
} else {
|
||||
performBatchUpdates({
|
||||
internalBatchUpdates(changesWithIndexSet: changesWithIndexSet)
|
||||
}, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper
|
||||
|
||||
private func internalBatchUpdates(changesWithIndexPath: ChangeWithIndexPath) {
|
||||
changesWithIndexPath.deletes.executeIfPresent {
|
||||
deleteItems(at: $0)
|
||||
}
|
||||
|
||||
changesWithIndexPath.inserts.executeIfPresent {
|
||||
insertItems(at: $0)
|
||||
}
|
||||
|
||||
changesWithIndexPath.moves.executeIfPresent {
|
||||
$0.forEach { move in
|
||||
moveItem(at: move.from, to: move.to)
|
||||
}
|
||||
}
|
||||
|
||||
changesWithIndexPath.replaces.executeIfPresent {
|
||||
reloadItems(at: $0)
|
||||
}
|
||||
}
|
||||
|
||||
private func internalBatchUpdates(changesWithIndexSet: ChangeWithIndexSet) {
|
||||
changesWithIndexSet.deletes.executeIfPresent {
|
||||
deleteSections($0)
|
||||
}
|
||||
|
||||
changesWithIndexSet.inserts.executeIfPresent {
|
||||
insertSections($0)
|
||||
}
|
||||
|
||||
changesWithIndexSet.moves.executeIfPresent {
|
||||
$0.forEach { move in
|
||||
moveSection(move.from, toSection: move.to)
|
||||
}
|
||||
}
|
||||
|
||||
changesWithIndexSet.replaces.executeIfPresent {
|
||||
reloadSections($0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
public extension UITableView {
|
||||
|
||||
/// Animate reload in a batch update
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - changes: The changes from diff
|
||||
/// - section: The section that all calculated IndexPath belong
|
||||
/// - insertionAnimation: The animation for insert rows
|
||||
/// - deletionAnimation: The animation for delete rows
|
||||
/// - replacementAnimation: The animation for reload rows
|
||||
/// - completion: Called when operation completes
|
||||
public func reload<T: Hashable>(
|
||||
changes: [Change<T>],
|
||||
section: Int = 0,
|
||||
insertionAnimation: UITableViewRowAnimation = .automatic,
|
||||
deletionAnimation: UITableViewRowAnimation = .automatic,
|
||||
replacementAnimation: UITableViewRowAnimation = .automatic,
|
||||
completion: @escaping (Bool) -> Void) {
|
||||
|
||||
let changesWithIndexPath = IndexPathConverter().convert(changes: changes, section: section)
|
||||
|
||||
// reloadRows needs to be called outside the batch
|
||||
|
||||
if #available(iOS 11, tvOS 11, *) {
|
||||
performBatchUpdates({
|
||||
internalBatchUpdates(changesWithIndexPath: changesWithIndexPath,
|
||||
insertionAnimation: insertionAnimation,
|
||||
deletionAnimation: deletionAnimation)
|
||||
}, completion: completion)
|
||||
|
||||
changesWithIndexPath.replaces.executeIfPresent {
|
||||
self.reloadRows(at: $0, with: replacementAnimation)
|
||||
}
|
||||
} else {
|
||||
beginUpdates()
|
||||
internalBatchUpdates(changesWithIndexPath: changesWithIndexPath,
|
||||
insertionAnimation: insertionAnimation,
|
||||
deletionAnimation: deletionAnimation)
|
||||
endUpdates()
|
||||
|
||||
changesWithIndexPath.replaces.executeIfPresent {
|
||||
reloadRows(at: $0, with: replacementAnimation)
|
||||
}
|
||||
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper
|
||||
|
||||
private func internalBatchUpdates(changesWithIndexPath: ChangeWithIndexPath,
|
||||
insertionAnimation: UITableViewRowAnimation,
|
||||
deletionAnimation: UITableViewRowAnimation) {
|
||||
changesWithIndexPath.deletes.executeIfPresent {
|
||||
deleteRows(at: $0, with: deletionAnimation)
|
||||
}
|
||||
|
||||
changesWithIndexPath.inserts.executeIfPresent {
|
||||
insertRows(at: $0, with: insertionAnimation)
|
||||
}
|
||||
|
||||
changesWithIndexPath.moves.executeIfPresent {
|
||||
$0.forEach { move in
|
||||
moveRow(at: move.from, to: move.to)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public enum Math {
|
||||
public static func lerp<T: FloatingPoint>(from: T, to: T, progress: T) -> T {
|
||||
@@ -34,3 +35,17 @@ public enum Progress {
|
||||
return centeredProgress
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public enum Inertia {
|
||||
|
||||
public static func applyResistance(for source: CGFloat, with scrollPosition: CGFloat, decelerationRate: UIScrollView.DecelerationRate = .fast, maximumScrollDistance: CGFloat = 120) -> CGFloat {
|
||||
let resistantDistance = (decelerationRate.rawValue * abs(scrollPosition) * maximumScrollDistance) / (maximumScrollDistance + decelerationRate.rawValue * abs(scrollPosition))
|
||||
return source + (scrollPosition < 0 ? -resistantDistance : resistantDistance)
|
||||
}
|
||||
|
||||
// Distance travelled after deceleration to zero velocity at a constant rate
|
||||
public static func project(initialVelocity: CGFloat, decelerationRate: UIScrollView.DecelerationRate = .fast) -> CGFloat {
|
||||
return (initialVelocity / 1000.0) * decelerationRate.rawValue / (1.0 - decelerationRate.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import UIKit
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
|
||||
public func addAttributes(for string: String, attributes: [NSAttributedStringKey : Any]) -> Self {
|
||||
public func addAttributes(for string: String, attributes: [NSAttributedString.Key : Any]) -> Self {
|
||||
let range = (self.string as NSString).range(of: string)
|
||||
addAttributes(attributes, range: range)
|
||||
return self
|
||||
@@ -12,9 +12,9 @@ extension NSMutableAttributedString {
|
||||
public func setAsLink(textToFind:String, linkURL:String, color: UIColor, font: UIFont? = nil) -> Self {
|
||||
let foundRange = self.mutableString.range(of: textToFind)
|
||||
if foundRange.location != NSNotFound {
|
||||
self.addAttribute(NSAttributedStringKey.link, value: linkURL, range: foundRange)
|
||||
self.addAttribute(NSAttributedStringKey.foregroundColor, value: color, range: foundRange)
|
||||
if let font = font { self.addAttribute(NSAttributedStringKey.font, value: font, range: foundRange) }
|
||||
self.addAttribute(NSAttributedString.Key.link, value: linkURL, range: foundRange)
|
||||
self.addAttribute(NSAttributedString.Key.foregroundColor, value: color, range: foundRange)
|
||||
if let font = font { self.addAttribute(NSAttributedString.Key.font, value: font, range: foundRange) }
|
||||
}
|
||||
return self
|
||||
}
|
||||
@@ -28,11 +28,4 @@ extension NSAttributedString {
|
||||
let result = boundingRect(with: size, options: [ .usesLineFragmentOrigin, .usesFontLeading ], context: nil).size
|
||||
return result
|
||||
}
|
||||
|
||||
// public static func + (left: NSAttributedString, right: NSAttributedString) -> NSAttributedString {
|
||||
// return (left.mutableCopy() as! NSMutableAttributedString).then {
|
||||
// $0.append(right)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,13 @@ extension CAMediaTimingFunction {
|
||||
public static func timingFunction(withCurve curve: TimingFunction) -> CAMediaTimingFunction {
|
||||
switch curve {
|
||||
case .linear:
|
||||
return CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
|
||||
return CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
||||
case .easeIn:
|
||||
return CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
|
||||
return CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
|
||||
case .easeOut:
|
||||
return CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
|
||||
return CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
|
||||
case .easeInOut:
|
||||
return CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
|
||||
return CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
|
||||
case .spring:
|
||||
return CAMediaTimingFunction(controlPoints: 0.5, 1.1 + Float(1/3), 1, 1)
|
||||
case .easeInSine:
|
||||
|
||||
Reference in New Issue
Block a user