├── .circleci └── config.yml ├── .gitignore ├── CHANGELOG.md ├── Example ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── attach-button.imageset │ │ ├── Contents.json │ │ ├── attach-button.png │ │ ├── attach-button@2x.png │ │ └── attach-button@3x.png │ └── delete-button.imageset │ │ ├── Contents.json │ │ ├── delete-button.png │ │ ├── delete-button@2x.png │ │ └── delete-button@3x.png ├── Base.lproj │ └── LaunchScreen.storyboard ├── ImageAttachmentView.swift ├── ImageAttachmentViewController.swift ├── ImageThumbnailView.swift ├── Info.plist ├── LabeledTextField.swift ├── LabeledTextFieldController.swift └── ViewController.swift ├── LICENSE.md ├── Makefile ├── Package.swift ├── README.md ├── SeedStackViewController.podspec ├── StackViewController.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Example.xcscheme │ └── StackViewController.xcscheme ├── StackViewController ├── AutoScrollView.swift ├── ContentWidth.swift ├── Info.plist ├── SeparatorView.swift ├── StackViewContainer.swift ├── StackViewController.h ├── StackViewController.swift ├── StackViewItem.swift ├── UIStackViewExtensions.swift └── UIViewExtensions.swift ├── StackViewControllerTests ├── Info.plist ├── StackViewContainerTests.swift └── StackViewControllerTests.swift └── screenshot.png /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build-and-test: 4 | 5 | macos: 6 | xcode: 11.1.0 7 | 8 | steps: 9 | - checkout 10 | - run: make clean ci 11 | 12 | workflows: 13 | version: 2 14 | build-and-test: 15 | jobs: 16 | - build-and-test 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | 28 | ## Playgrounds 29 | timeline.xctimeline 30 | playground.xcworkspace 31 | 32 | # Swift Package Manager 33 | # 34 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 35 | # Packages/ 36 | .build/ 37 | 38 | # CocoaPods 39 | # 40 | # We recommend against adding the Pods directory to your .gitignore. However 41 | # you should judge for yourself, the pros and cons are mentioned at: 42 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 43 | # 44 | # Pods/ 45 | 46 | # Carthage 47 | # 48 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 49 | # Carthage/Checkouts 50 | 51 | Carthage/Build 52 | 53 | # fastlane 54 | # 55 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 56 | # screenshots whenever they are needed. 57 | # For more information about the recommended setup visit: 58 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 59 | 60 | fastlane/report.xml 61 | fastlane/Preview.html 62 | fastlane/screenshots 63 | fastlane/test_output 64 | 65 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [0.5.0](https://github.com/seedco/StackViewController/compare/0.4.0...0.5.0) 2 | 3 | - Upgrade to Swift 5.0. 4 | - Fix runtime crashes (unrecognized selector) by adding `@objc` annotation to public UIKit extensions. 5 | -------------------------------------------------------------------------------- /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Indragie Karunaratne on 2016-04-16. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | var window: UIWindow? 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | self.window = { 17 | let window = UIWindow(frame: UIScreen.main.bounds) 18 | window.backgroundColor = .white 19 | window.rootViewController = UINavigationController(rootViewController: ViewController()) 20 | window.makeKeyAndVisible() 21 | return window 22 | }() 23 | return true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/attach-button.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "attach-button.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "attach-button@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "attach-button@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/attach-button.imageset/attach-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seedco/StackViewController/b88e7a855ff6a08739a7b99a2825f059bd916152/Example/Assets.xcassets/attach-button.imageset/attach-button.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/attach-button.imageset/attach-button@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seedco/StackViewController/b88e7a855ff6a08739a7b99a2825f059bd916152/Example/Assets.xcassets/attach-button.imageset/attach-button@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/attach-button.imageset/attach-button@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seedco/StackViewController/b88e7a855ff6a08739a7b99a2825f059bd916152/Example/Assets.xcassets/attach-button.imageset/attach-button@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/delete-button.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "delete-button.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "delete-button@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "delete-button@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/delete-button.imageset/delete-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seedco/StackViewController/b88e7a855ff6a08739a7b99a2825f059bd916152/Example/Assets.xcassets/delete-button.imageset/delete-button.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/delete-button.imageset/delete-button@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seedco/StackViewController/b88e7a855ff6a08739a7b99a2825f059bd916152/Example/Assets.xcassets/delete-button.imageset/delete-button@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/delete-button.imageset/delete-button@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seedco/StackViewController/b88e7a855ff6a08739a7b99a2825f059bd916152/Example/Assets.xcassets/delete-button.imageset/delete-button@3x.png -------------------------------------------------------------------------------- /Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Example/ImageAttachmentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageAttachmentView.swift 3 | // StackViewController 4 | // 5 | // Created by Indragie Karunaratne on 2016-04-24. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import StackViewController 11 | 12 | class ImageAttachmentView: UIView, ImageThumbnailViewDelegate { 13 | fileprivate struct Layout { 14 | static let ContainerInsets = UIEdgeInsets(top: 15, left: 0, bottom: 15, right: 0) 15 | static let ScrollViewInsets = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15) 16 | static let StackViewSpacing: CGFloat = 10 17 | } 18 | 19 | let attachButton: UIButton 20 | fileprivate let stackViewContainer: StackViewContainer 21 | 22 | override init(frame: CGRect) { 23 | attachButton = UIButton(type: .custom) 24 | attachButton.setBackgroundImage(UIImage(named: "attach-button")!, for: UIControl.State()) 25 | attachButton.adjustsImageWhenHighlighted = true 26 | 27 | let stackView = UIStackView(frame: CGRect.zero) 28 | stackView.axis = .horizontal 29 | stackView.alignment = .bottom 30 | 31 | stackViewContainer = StackViewContainer(stackView: stackView) 32 | stackViewContainer.addContentView(attachButton) 33 | 34 | super.init(frame: frame) 35 | 36 | let scrollView = stackViewContainer.scrollView 37 | scrollView.alwaysBounceHorizontal = true 38 | scrollView.contentInset = Layout.ScrollViewInsets 39 | scrollView.scrollIndicatorInsets = Layout.ScrollViewInsets 40 | 41 | addSubview(stackViewContainer) 42 | _ = stackViewContainer.activateSuperviewHuggingConstraints(insets: Layout.ContainerInsets) 43 | } 44 | 45 | required init?(coder aDecoder: NSCoder) { 46 | fatalError("init(coder:) has not been implemented") 47 | } 48 | 49 | func addImageWithThumbnail(_ thumbnail: UIImage) { 50 | let thumbnailView = ImageThumbnailView(thumbnail: thumbnail) 51 | thumbnailView.delegate = self 52 | stackViewContainer.addContentView(thumbnailView) 53 | } 54 | 55 | // MARK: ImageThumbnailViewDelegate 56 | 57 | func imageThumbnailViewDidTapDeleteButton(_ view: ImageThumbnailView) { 58 | stackViewContainer.removeContentView(view) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Example/ImageAttachmentViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageAttachmentViewController.swift 3 | // StackViewController 4 | // 5 | // Created by Indragie Karunaratne on 2016-04-24. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | class ImageAttachmentViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { 13 | fileprivate struct Constants { 14 | static let ThumbnailSize = CGSize(width: 96, height: 96) 15 | } 16 | 17 | fileprivate var attachmentView: ImageAttachmentView? 18 | 19 | convenience init() { 20 | self.init(nibName: nil, bundle: nil) 21 | } 22 | 23 | override func loadView() { 24 | let attachmentView = ImageAttachmentView(frame: CGRect.zero) 25 | attachmentView.attachButton.addTarget(self, action: #selector(ImageAttachmentViewController.attachImage(_:)), for: .touchUpInside) 26 | view = attachmentView 27 | self.attachmentView = attachmentView 28 | } 29 | 30 | // MARK: Actions 31 | 32 | @objc fileprivate func attachImage(_ sender: UIButton) { 33 | guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else { return } 34 | let pickerController = UIImagePickerController() 35 | pickerController.delegate = self 36 | pickerController.sourceType = .photoLibrary 37 | present(pickerController, animated: true, completion: nil) 38 | } 39 | 40 | // MARK: UIImagePickerControllerDelegate 41 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { 42 | dismiss(animated: true, completion: nil) 43 | guard let imageURL = info[.referenceURL] as? URL else { return } 44 | getImageThumbnail(imageURL) { image in 45 | if let image = image { 46 | self.attachmentView?.addImageWithThumbnail(image) 47 | } 48 | } 49 | } 50 | 51 | fileprivate func getImageThumbnail(_ imageURL: URL, completion: @escaping (UIImage?) -> Void) { 52 | let result = PHAsset.fetchAssets(withALAssetURLs: [imageURL], options: nil) 53 | guard let asset = result.firstObject else { 54 | completion(nil) 55 | return 56 | } 57 | let imageManager = PHImageManager.default() 58 | let options = PHImageRequestOptions() 59 | options.resizeMode = .exact 60 | options.deliveryMode = .highQualityFormat 61 | 62 | let scale = UIScreen.main.scale 63 | let targetSize: CGSize = { 64 | var targetSize = Constants.ThumbnailSize 65 | targetSize.width *= scale 66 | targetSize.height *= scale 67 | return targetSize 68 | }() 69 | 70 | imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { (image, info) in 71 | let degraded = info?[PHImageResultIsDegradedKey] as? Bool 72 | if degraded == nil || degraded! == false { 73 | if let image = image, let CGImage = image.cgImage { 74 | let scaleCorrectedImage = UIImage(cgImage: CGImage, scale: scale, orientation: image.imageOrientation) 75 | completion(scaleCorrectedImage) 76 | } else { 77 | completion(nil) 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Example/ImageThumbnailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageThumbnailView.swift 3 | // StackViewController 4 | // 5 | // Created by Indragie Karunaratne on 2016-04-24. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import StackViewController 11 | 12 | protocol ImageThumbnailViewDelegate: AnyObject { 13 | func imageThumbnailViewDidTapDeleteButton(_ view: ImageThumbnailView) 14 | } 15 | 16 | open class ImageThumbnailView: UIView { 17 | fileprivate struct Appearance { 18 | static let ImageCornerRadius: CGFloat = 8.0 19 | } 20 | 21 | weak var delegate: ImageThumbnailViewDelegate? 22 | 23 | init(thumbnail: UIImage) { 24 | super.init(frame: CGRect.zero) 25 | 26 | let deleteButtonImage = UIImage(named: "delete-button")! 27 | let deleteButton = UIButton(type: .custom) 28 | deleteButton.translatesAutoresizingMaskIntoConstraints = false 29 | deleteButton.setBackgroundImage(deleteButtonImage, for: UIControl.State()) 30 | deleteButton.addTarget(self, action: #selector(ImageThumbnailView.didTapDelete(_:)), for: .touchUpInside) 31 | 32 | let imageView = UIImageView(image: thumbnail) 33 | imageView.layer.cornerRadius = Appearance.ImageCornerRadius 34 | imageView.translatesAutoresizingMaskIntoConstraints = false 35 | 36 | addSubview(imageView) 37 | addSubview(deleteButton) 38 | 39 | let metrics = [ 40 | "imageViewLeft": -(deleteButtonImage.size.width / 2), 41 | "imageViewTop": -(deleteButtonImage.size.height / 2) 42 | ] 43 | let views = [ 44 | "deleteButton": deleteButton, 45 | "imageView": imageView 46 | ] 47 | let verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|[deleteButton]-imageViewTop-[imageView]|", options: [], metrics: metrics, views: views) 48 | let horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|[deleteButton]-imageViewLeft-[imageView]|", options: [], metrics: metrics, views: views) 49 | NSLayoutConstraint.activate(verticalConstraints) 50 | NSLayoutConstraint.activate(horizontalConstraints) 51 | } 52 | 53 | required public init?(coder aDecoder: NSCoder) { 54 | fatalError("init(coder:) has not been implemented") 55 | } 56 | 57 | // MARK: Actions 58 | 59 | @objc fileprivate func didTapDelete(_ sender: UIButton) { 60 | delegate?.imageThumbnailViewDidTapDeleteButton(self) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSPhotoLibraryUsageDescription 26 | Photo Library Access 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | UIInterfaceOrientationPortraitUpsideDown 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Example/LabeledTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabeledTextField.swift 3 | // StackViewController 4 | // 5 | // Created by Indragie Karunaratne on 2016-04-24. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import StackViewController 11 | 12 | class LabeledTextField: UIView { 13 | fileprivate struct Appearance { 14 | static let LabelTextColor = UIColor(white: 0.56, alpha: 1.0) 15 | static let FieldTextColor = UIColor.black 16 | static let Font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body) 17 | } 18 | 19 | fileprivate struct Layout { 20 | static let EdgeInsets = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15) 21 | static let StackViewSpacing: CGFloat = 10 22 | } 23 | 24 | let label: UILabel 25 | let textField: UITextField 26 | 27 | init(labelText: String) { 28 | label = UILabel(frame: CGRect.zero) 29 | label.textColor = Appearance.LabelTextColor 30 | label.font = Appearance.Font 31 | label.text = labelText 32 | label.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .horizontal) 33 | 34 | textField = UITextField(frame: CGRect.zero) 35 | textField.textColor = Appearance.FieldTextColor 36 | textField.font = Appearance.Font 37 | 38 | super.init(frame: CGRect.zero) 39 | 40 | let stackView = UIStackView(arrangedSubviews: [label, textField]) 41 | stackView.axis = .horizontal 42 | stackView.spacing = Layout.StackViewSpacing 43 | 44 | addSubview(stackView) 45 | _ = stackView.activateSuperviewHuggingConstraints(insets: Layout.EdgeInsets) 46 | } 47 | 48 | required init?(coder aDecoder: NSCoder) { 49 | fatalError("init(coder:) has not been implemented") 50 | } 51 | 52 | override func becomeFirstResponder() -> Bool { 53 | textField.becomeFirstResponder() 54 | return false 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Example/LabeledTextFieldController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabeledTextFieldController.swift 3 | // StackViewController 4 | // 5 | // Created by Indragie Karunaratne on 2016-04-24. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LabeledTextFieldController: UIViewController, UITextFieldDelegate { 12 | fileprivate let labelText: String 13 | 14 | init(labelText: String) { 15 | self.labelText = labelText 16 | super.init(nibName: nil, bundle: nil) 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | override func loadView() { 24 | let labeledField = LabeledTextField(labelText: labelText) 25 | labeledField.textField.delegate = self 26 | view = labeledField 27 | } 28 | 29 | // MARK: UITextFieldDelegate 30 | 31 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 32 | textField.resignFirstResponder() 33 | return false 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example 4 | // 5 | // Created by Indragie Karunaratne on 2016-04-16. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import StackViewController 11 | 12 | class ViewController: UIViewController { 13 | fileprivate let stackViewController: StackViewController 14 | fileprivate var firstField: UIView? 15 | fileprivate var bodyTextView: UITextView? 16 | 17 | init() { 18 | stackViewController = StackViewController() 19 | stackViewController.separatorViewFactory = StackViewContainer.createSeparatorViewFactory() 20 | 21 | super.init(nibName: nil, bundle: nil) 22 | 23 | edgesForExtendedLayout = UIRectEdge() 24 | title = "Send Message" 25 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Send", style: .done, target: self, action: #selector(ViewController.send(_:))) 26 | } 27 | 28 | required init?(coder aDecoder: NSCoder) { 29 | fatalError("init(coder:) has not been implemented") 30 | } 31 | 32 | override func loadView() { 33 | view = UIView(frame: CGRect.zero) 34 | view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(ViewController.didTapView))) 35 | } 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | setupStackViewController() 40 | displayStackViewController() 41 | } 42 | 43 | fileprivate func setupStackViewController() { 44 | let toFieldController = LabeledTextFieldController(labelText: "To:") 45 | firstField = toFieldController.view 46 | stackViewController.addItem(toFieldController) 47 | stackViewController.addItem(LabeledTextFieldController(labelText: "Subject:")) 48 | 49 | let textView = UITextView(frame: CGRect.zero) 50 | textView.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body) 51 | textView.isScrollEnabled = false 52 | textView.textContainerInset = UIEdgeInsets(top: 15, left: 10, bottom: 0, right: 10) 53 | textView.text = "This field automatically expands as you type, no additional logic required" 54 | stackViewController.addItem(textView, canShowSeparator: false) 55 | bodyTextView = textView 56 | 57 | stackViewController.addItem(ImageAttachmentViewController()) 58 | } 59 | 60 | fileprivate func displayStackViewController() { 61 | addChild(stackViewController) 62 | view.addSubview(stackViewController.view) 63 | _ = stackViewController.view.activateSuperviewHuggingConstraints() 64 | stackViewController.didMove(toParent: self) 65 | } 66 | 67 | override func viewDidAppear(_ animated: Bool) { 68 | super.viewDidAppear(animated) 69 | firstField?.becomeFirstResponder() 70 | } 71 | 72 | // MARK: Actions 73 | 74 | @objc fileprivate func send(_ sender: UIBarButtonItem) {} 75 | 76 | @objc fileprivate func didTapView(_ gestureRecognizer: UIGestureRecognizer) { 77 | bodyTextView?.becomeFirstResponder() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | **Copyright (c) 2016 Seed Platform, Inc.** 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SDK="iphonesimulator" 2 | DESTINATION="platform=iOS Simulator,name=iPhone 8" 3 | PROJECT="StackViewController" 4 | SCHEME="StackViewController" 5 | 6 | .PHONY: all build test 7 | 8 | build: 9 | set -o pipefail && \ 10 | xcodebuild \ 11 | -sdk $(SDK) \ 12 | -derivedDataPath build \ 13 | -project $(PROJECT).xcodeproj \ 14 | -scheme $(SCHEME) \ 15 | -configuration Debug \ 16 | -destination $(DESTINATION) \ 17 | build | xcpretty 18 | 19 | test: 20 | set -o pipefail && \ 21 | xcodebuild \ 22 | -sdk $(SDK) \ 23 | -derivedDataPath build \ 24 | -project $(PROJECT).xcodeproj \ 25 | -scheme $(SCHEME) \ 26 | -configuration Debug \ 27 | -destination $(DESTINATION) \ 28 | test | xcpretty 29 | 30 | ci: 31 | $(MAKE) SCHEME=StackViewController test 32 | $(MAKE) SCHEME=Example build 33 | 34 | 35 | clean: 36 | rm -rf build 37 | 38 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "StackViewController", 7 | platforms: [.iOS(.v9)], 8 | products: [ 9 | .library( 10 | name: "StackViewController", 11 | targets: ["StackViewController"] 12 | ) 13 | ], 14 | targets: [ 15 | .target( 16 | name: "StackViewController", 17 | path: "StackViewController" 18 | ), 19 | .testTarget( 20 | name: "StackViewControllerTests", 21 | dependencies: ["StackViewController"], 22 | path: "StackViewControllerTests" 23 | ) 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StackViewController [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-brightgreen.svg?style=flat-square)](https://github.com/Carthage/Carthage) [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/SeedStackViewController.svg)](https://img.shields.io/cocoapods/v/SeedStackViewController.svg) 2 | 3 | ### Overview 4 | 5 | `StackViewController` is a Swift framework that simplifies the process of building forms and other static content using `UIStackView`. For example, the form below is implemented using a StackViewController: 6 | 7 |

8 | StackViewController Example App 9 |

10 | 11 | ### Design Rationale 12 | 13 | The purpose of this project is two-fold: encouraging design patterns that are more suitable for building content like the form pictured above, and providing tools to make the process simpler. The following sections contain a summary of the existing solutions and how we can improve upon them. 14 | 15 | #### Building Forms with `UITableView` (Is Difficult) 16 | 17 | Traditionally, iOS developers have utilized `UITableView` to build forms and other relatively static list-based user interfaces, despite the `UITableView` API being a poor fit for such tasks. `UITableView` is designed primarily for dynamic content, and a lot of the functionality that it provides is *only necessary* for dynamic content. Using it to build static user interfaces results in a lot of boilerplate code in implementing many data source and delegate methods. 18 | 19 | Another major issue is the difficulty of implementing variable-height content with `UITableView`. When building a form, for example, a common need is the ability to display a field (e.g. a text view) whose dimensions automatically change as the content inside it changes. One half of the problem is knowing how to size the cell — this is typically done by either manually computing the size in `-tableView:heightForRowAtIndexPath:` or by using Autolayout, estimated row heights, and the self-sizing table view cell feature introduced in iOS 8. The other half of the problem is notifying the table view that it should update the layout for a cell once the content inside the cell changes. This can involve ugly hacks like calling `-[UITableView beginUpdates]` and `-[UITableView endUpdates]` to force relayout. 20 | 21 | The bottom line is that **`UITableView` is the wrong tool for the job**. 22 | 23 | #### Introducing `UIStackView` 24 | 25 | [`UIStackView`](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIStackView_Class_Reference/), introduced in iOS 9, provides a clean abstraction over Autolayout for laying out a horizontal or vertical stack of views. By composing multiple instances of `UIStackView`, the vast majority of common user interface layouts can be built quite easily without the need to manually create and remove layout constraints. 26 | 27 | `UIStackView` is well suited for the task of building forms and other static content, but it has some shortcomings when applied to that particular use case. There are things that `UITableView` and `UITableViewController` provide that we often take for granted: scrolling support, cell separators, and other conveniences. `UIStackView` doesn't have this functionality built in, so one of the goals of this library is to fill in those key pieces of missing functionality to the point where using a stack view is *easier* than using a table view for the same task. 28 | 29 | #### View Controllers over Views 30 | 31 | A strong indicator of poorly designed iOS code is a bad separation of responsibilities between the view and the view controller, in accordance with the MVC (Model-View-Controller) pattern. The *Massive View Controller* anti-pattern is a common occurrence where the view controller simply does too much, absorbing responsibilities from the model and view layers. Conversely, there is also an anti-pattern where the view takes on many controller-like responsibilities rather than just focusing on the layout and rendering of the content. 32 | 33 | `StackViewController` defines a **single** API for using both `UIView` and `UIViewController` instances to provide content. `UIView` instances can be used when the content being displayed is simple and non-interactive (e.g. a static label). `UIViewController` instances can be used for more complex controls where there needs to be a controller in addition to the view, when, for example, a view displays a visual representation of state from a model that needs to be updated as the user interacts with the view. 34 | 35 | #### View Controller Composition 36 | 37 | [*Composition over inheritance*](https://en.wikipedia.org/wiki/Composition_over_inheritance) is a fundamental principle of object-oriented programming. 38 | 39 | This principle has always been used in iOS view hierarchies, where more complex views are composed out of simpler ones (e.g. how a `UIButton` contains a `UILabel` and a `UIImageView` that render its content). However, there was no "official" way to compose view controllers until the introduction of [view controller containment](https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html) in iOS 5. It was possible to mimic behaviour like this prior to iOS 5, but handling the propagation of events between parent and child view controllers and transitions between child view controllers was difficult to get right, which are all problems that the view controller containment API solves. 40 | 41 | In the same way that you can create complex layouts by composing multiple `UIStackView` instances, you can use the view controller containment API to compose multiple instances of `StackViewController` to create a hierarchy of view controllers where each content view is backed by a corresponding view controller that cleanly separates the responsibilities, instead of handling all of that at the view level (an anti-pattern, as mentioned earlier). 42 | 43 | ### Features 44 | 45 | The framework provides two primary classes: `StackViewContainer` and `StackViewController`. `StackViewContainer` wraps a `UIStackView` and implements the following additional features: 46 | 47 | * **Scrolling support** by embedding the `UIStackView` inside a `UIScrollView` with automatic management of associated constraints 48 | * **Autoscroll behaviour** to automatically adjust layout and scroll to the view being edited when the keyboard appears (the same behaviour implemented by `UITableViewController`) 49 | * **Customizable separator views** between content views that can be toggled on a per-view basis and are managed automatically when content views are inserted and removed 50 | * Other minor conveniences like support for background views and changing the background color (since `UIStackView` doesn't draw a background) 51 | 52 | `StackViewController` is a subclass of `UIViewController` that uses an instance of `StackViewContainer` as its view, and adds support for adding content using [view controller containment](https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html) (i.e. view controller composition). This means that you can use view controllers and/or views to represent your content instead of just views, and `StackViewController` automatically handles adding and removing them as child view controllers. 53 | 54 | ### Example 55 | 56 | The included example app, pictured above, demonstrates the usage of both `StackViewContainer` on its own (the image attachment control) as well as `StackViewController` (the full form). 57 | 58 | ### License 59 | 60 | This project is licensed under the MIT license. See `LICENSE.md` for more details. 61 | -------------------------------------------------------------------------------- /SeedStackViewController.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "SeedStackViewController" 3 | s.module_name = "StackViewController" 4 | s.version = "0.6.0" 5 | s.summary = "Simplifies the process of building forms and other static content using UIStackView." 6 | s.description = "StackViewController is a Swift framework that simplifies the process of building forms and other static content using UIStackView." 7 | s.homepage = "https://github.com/seedco/StackViewController" 8 | s.license = "MIT" 9 | s.author = "Seed" 10 | s.source = { 11 | git: "https://github.com/seedco/StackViewController.git", 12 | tag: s.version.to_s 13 | } 14 | s.ios.deployment_target = "12.0" 15 | s.source_files = "StackViewController/*.{h,swift}" 16 | s.frameworks = "UIKit" 17 | s.swift_versions = "5.0" 18 | end 19 | -------------------------------------------------------------------------------- /StackViewController.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 72D193E71CC3576A00645F83 /* StackViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 72D193E61CC3576A00645F83 /* StackViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 72D193EE1CC3576A00645F83 /* StackViewController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 72D193E41CC3576A00645F83 /* StackViewController.framework */; }; 12 | 72D193F31CC3576A00645F83 /* StackViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72D193F21CC3576A00645F83 /* StackViewControllerTests.swift */; }; 13 | 72D194011CC3577B00645F83 /* AutoScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72D193FB1CC3577B00645F83 /* AutoScrollView.swift */; }; 14 | 72D194031CC3577B00645F83 /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72D193FD1CC3577B00645F83 /* SeparatorView.swift */; }; 15 | 72D194041CC3577B00645F83 /* StackViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72D193FE1CC3577B00645F83 /* StackViewContainer.swift */; }; 16 | 72D194051CC3577B00645F83 /* StackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72D193FF1CC3577B00645F83 /* StackViewController.swift */; }; 17 | 72D194061CC3577B00645F83 /* StackViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72D194001CC3577B00645F83 /* StackViewItem.swift */; }; 18 | 72D1940E1CC357A100645F83 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72D1940D1CC357A100645F83 /* AppDelegate.swift */; }; 19 | 72D194101CC357A100645F83 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72D1940F1CC357A100645F83 /* ViewController.swift */; }; 20 | 72D194151CC357A100645F83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 72D194141CC357A100645F83 /* Assets.xcassets */; }; 21 | 72D194181CC357A100645F83 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 72D194161CC357A100645F83 /* LaunchScreen.storyboard */; }; 22 | 72D1941D1CC357A800645F83 /* StackViewController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 72D193E41CC3576A00645F83 /* StackViewController.framework */; }; 23 | 72D1941E1CC357A800645F83 /* StackViewController.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 72D193E41CC3576A00645F83 /* StackViewController.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 24 | 72D194231CC35CDC00645F83 /* StackViewContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72D194221CC35CDC00645F83 /* StackViewContainerTests.swift */; }; 25 | 72DE371F1CCDA961009CAE32 /* LabeledTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72DE371E1CCDA961009CAE32 /* LabeledTextField.swift */; }; 26 | 72DE37211CCDABA8009CAE32 /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72DE37201CCDABA8009CAE32 /* UIViewExtensions.swift */; }; 27 | 72DE37231CCDB9AE009CAE32 /* LabeledTextFieldController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72DE37221CCDB9AE009CAE32 /* LabeledTextFieldController.swift */; }; 28 | 72DE37251CCDC75D009CAE32 /* ImageAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72DE37241CCDC75D009CAE32 /* ImageAttachmentView.swift */; }; 29 | 72DE37271CCDC769009CAE32 /* ImageAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72DE37261CCDC769009CAE32 /* ImageAttachmentViewController.swift */; }; 30 | 72DE37291CCDCCB6009CAE32 /* UIStackViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72DE37281CCDCCB6009CAE32 /* UIStackViewExtensions.swift */; }; 31 | 72DE372B1CCDD998009CAE32 /* ImageThumbnailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72DE372A1CCDD998009CAE32 /* ImageThumbnailView.swift */; }; 32 | 756AE907270B9DA000644AC5 /* ContentWidth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 756AE906270B9DA000644AC5 /* ContentWidth.swift */; }; 33 | /* End PBXBuildFile section */ 34 | 35 | /* Begin PBXContainerItemProxy section */ 36 | 72D193EF1CC3576A00645F83 /* PBXContainerItemProxy */ = { 37 | isa = PBXContainerItemProxy; 38 | containerPortal = 72B21E4D1CBC34EF00724EB8 /* Project object */; 39 | proxyType = 1; 40 | remoteGlobalIDString = 72D193E31CC3576A00645F83; 41 | remoteInfo = StackViewController; 42 | }; 43 | 72D1941F1CC357A800645F83 /* PBXContainerItemProxy */ = { 44 | isa = PBXContainerItemProxy; 45 | containerPortal = 72B21E4D1CBC34EF00724EB8 /* Project object */; 46 | proxyType = 1; 47 | remoteGlobalIDString = 72D193E31CC3576A00645F83; 48 | remoteInfo = StackViewController; 49 | }; 50 | /* End PBXContainerItemProxy section */ 51 | 52 | /* Begin PBXCopyFilesBuildPhase section */ 53 | 72D194211CC357A800645F83 /* Embed Frameworks */ = { 54 | isa = PBXCopyFilesBuildPhase; 55 | buildActionMask = 2147483647; 56 | dstPath = ""; 57 | dstSubfolderSpec = 10; 58 | files = ( 59 | 72D1941E1CC357A800645F83 /* StackViewController.framework in Embed Frameworks */, 60 | ); 61 | name = "Embed Frameworks"; 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXCopyFilesBuildPhase section */ 65 | 66 | /* Begin PBXFileReference section */ 67 | 72D193E41CC3576A00645F83 /* StackViewController.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StackViewController.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 68 | 72D193E61CC3576A00645F83 /* StackViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StackViewController.h; sourceTree = ""; }; 69 | 72D193E81CC3576A00645F83 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 70 | 72D193ED1CC3576A00645F83 /* StackViewControllerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StackViewControllerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 71 | 72D193F21CC3576A00645F83 /* StackViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackViewControllerTests.swift; sourceTree = ""; }; 72 | 72D193F41CC3576A00645F83 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 73 | 72D193FB1CC3577B00645F83 /* AutoScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoScrollView.swift; sourceTree = ""; }; 74 | 72D193FD1CC3577B00645F83 /* SeparatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeparatorView.swift; sourceTree = ""; }; 75 | 72D193FE1CC3577B00645F83 /* StackViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackViewContainer.swift; sourceTree = ""; }; 76 | 72D193FF1CC3577B00645F83 /* StackViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackViewController.swift; sourceTree = ""; }; 77 | 72D194001CC3577B00645F83 /* StackViewItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackViewItem.swift; sourceTree = ""; }; 78 | 72D1940B1CC357A100645F83 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 79 | 72D1940D1CC357A100645F83 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 80 | 72D1940F1CC357A100645F83 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 81 | 72D194141CC357A100645F83 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 82 | 72D194171CC357A100645F83 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 83 | 72D194191CC357A100645F83 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 84 | 72D194221CC35CDC00645F83 /* StackViewContainerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackViewContainerTests.swift; sourceTree = ""; }; 85 | 72DE371E1CCDA961009CAE32 /* LabeledTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabeledTextField.swift; sourceTree = ""; }; 86 | 72DE37201CCDABA8009CAE32 /* UIViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = ""; }; 87 | 72DE37221CCDB9AE009CAE32 /* LabeledTextFieldController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabeledTextFieldController.swift; sourceTree = ""; }; 88 | 72DE37241CCDC75D009CAE32 /* ImageAttachmentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageAttachmentView.swift; sourceTree = ""; }; 89 | 72DE37261CCDC769009CAE32 /* ImageAttachmentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageAttachmentViewController.swift; sourceTree = ""; }; 90 | 72DE37281CCDCCB6009CAE32 /* UIStackViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIStackViewExtensions.swift; sourceTree = ""; }; 91 | 72DE372A1CCDD998009CAE32 /* ImageThumbnailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageThumbnailView.swift; sourceTree = ""; }; 92 | 756AE906270B9DA000644AC5 /* ContentWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWidth.swift; sourceTree = ""; }; 93 | /* End PBXFileReference section */ 94 | 95 | /* Begin PBXFrameworksBuildPhase section */ 96 | 72D193E01CC3576A00645F83 /* Frameworks */ = { 97 | isa = PBXFrameworksBuildPhase; 98 | buildActionMask = 2147483647; 99 | files = ( 100 | ); 101 | runOnlyForDeploymentPostprocessing = 0; 102 | }; 103 | 72D193EA1CC3576A00645F83 /* Frameworks */ = { 104 | isa = PBXFrameworksBuildPhase; 105 | buildActionMask = 2147483647; 106 | files = ( 107 | 72D193EE1CC3576A00645F83 /* StackViewController.framework in Frameworks */, 108 | ); 109 | runOnlyForDeploymentPostprocessing = 0; 110 | }; 111 | 72D194081CC357A100645F83 /* Frameworks */ = { 112 | isa = PBXFrameworksBuildPhase; 113 | buildActionMask = 2147483647; 114 | files = ( 115 | 72D1941D1CC357A800645F83 /* StackViewController.framework in Frameworks */, 116 | ); 117 | runOnlyForDeploymentPostprocessing = 0; 118 | }; 119 | /* End PBXFrameworksBuildPhase section */ 120 | 121 | /* Begin PBXGroup section */ 122 | 72B21E4C1CBC34EF00724EB8 = { 123 | isa = PBXGroup; 124 | children = ( 125 | 72D193E51CC3576A00645F83 /* StackViewController */, 126 | 72D193F11CC3576A00645F83 /* StackViewControllerTests */, 127 | 72D1940C1CC357A100645F83 /* Example */, 128 | 72B21E561CBC34EF00724EB8 /* Products */, 129 | ); 130 | indentWidth = 4; 131 | sourceTree = ""; 132 | tabWidth = 4; 133 | }; 134 | 72B21E561CBC34EF00724EB8 /* Products */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 72D193E41CC3576A00645F83 /* StackViewController.framework */, 138 | 72D193ED1CC3576A00645F83 /* StackViewControllerTests.xctest */, 139 | 72D1940B1CC357A100645F83 /* Example.app */, 140 | ); 141 | name = Products; 142 | sourceTree = ""; 143 | }; 144 | 72D193E51CC3576A00645F83 /* StackViewController */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | 72D193E61CC3576A00645F83 /* StackViewController.h */, 148 | 72D193FB1CC3577B00645F83 /* AutoScrollView.swift */, 149 | 756AE906270B9DA000644AC5 /* ContentWidth.swift */, 150 | 72D193FD1CC3577B00645F83 /* SeparatorView.swift */, 151 | 72D193FE1CC3577B00645F83 /* StackViewContainer.swift */, 152 | 72D193FF1CC3577B00645F83 /* StackViewController.swift */, 153 | 72D194001CC3577B00645F83 /* StackViewItem.swift */, 154 | 72D193E81CC3576A00645F83 /* Info.plist */, 155 | 72DE37201CCDABA8009CAE32 /* UIViewExtensions.swift */, 156 | 72DE37281CCDCCB6009CAE32 /* UIStackViewExtensions.swift */, 157 | ); 158 | path = StackViewController; 159 | sourceTree = ""; 160 | }; 161 | 72D193F11CC3576A00645F83 /* StackViewControllerTests */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 72D193F21CC3576A00645F83 /* StackViewControllerTests.swift */, 165 | 72D194221CC35CDC00645F83 /* StackViewContainerTests.swift */, 166 | 72D193F41CC3576A00645F83 /* Info.plist */, 167 | ); 168 | path = StackViewControllerTests; 169 | sourceTree = ""; 170 | }; 171 | 72D1940C1CC357A100645F83 /* Example */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | 72D1940D1CC357A100645F83 /* AppDelegate.swift */, 175 | 72D1940F1CC357A100645F83 /* ViewController.swift */, 176 | 72D194141CC357A100645F83 /* Assets.xcassets */, 177 | 72D194161CC357A100645F83 /* LaunchScreen.storyboard */, 178 | 72D194191CC357A100645F83 /* Info.plist */, 179 | 72DE371E1CCDA961009CAE32 /* LabeledTextField.swift */, 180 | 72DE37221CCDB9AE009CAE32 /* LabeledTextFieldController.swift */, 181 | 72DE37241CCDC75D009CAE32 /* ImageAttachmentView.swift */, 182 | 72DE37261CCDC769009CAE32 /* ImageAttachmentViewController.swift */, 183 | 72DE372A1CCDD998009CAE32 /* ImageThumbnailView.swift */, 184 | ); 185 | path = Example; 186 | sourceTree = ""; 187 | }; 188 | /* End PBXGroup section */ 189 | 190 | /* Begin PBXHeadersBuildPhase section */ 191 | 72D193E11CC3576A00645F83 /* Headers */ = { 192 | isa = PBXHeadersBuildPhase; 193 | buildActionMask = 2147483647; 194 | files = ( 195 | 72D193E71CC3576A00645F83 /* StackViewController.h in Headers */, 196 | ); 197 | runOnlyForDeploymentPostprocessing = 0; 198 | }; 199 | /* End PBXHeadersBuildPhase section */ 200 | 201 | /* Begin PBXNativeTarget section */ 202 | 72D193E31CC3576A00645F83 /* StackViewController */ = { 203 | isa = PBXNativeTarget; 204 | buildConfigurationList = 72D193F51CC3576A00645F83 /* Build configuration list for PBXNativeTarget "StackViewController" */; 205 | buildPhases = ( 206 | 72D193DF1CC3576A00645F83 /* Sources */, 207 | 72D193E01CC3576A00645F83 /* Frameworks */, 208 | 72D193E11CC3576A00645F83 /* Headers */, 209 | 72D193E21CC3576A00645F83 /* Resources */, 210 | ); 211 | buildRules = ( 212 | ); 213 | dependencies = ( 214 | ); 215 | name = StackViewController; 216 | productName = StackViewController; 217 | productReference = 72D193E41CC3576A00645F83 /* StackViewController.framework */; 218 | productType = "com.apple.product-type.framework"; 219 | }; 220 | 72D193EC1CC3576A00645F83 /* StackViewControllerTests */ = { 221 | isa = PBXNativeTarget; 222 | buildConfigurationList = 72D193F81CC3576A00645F83 /* Build configuration list for PBXNativeTarget "StackViewControllerTests" */; 223 | buildPhases = ( 224 | 72D193E91CC3576A00645F83 /* Sources */, 225 | 72D193EA1CC3576A00645F83 /* Frameworks */, 226 | 72D193EB1CC3576A00645F83 /* Resources */, 227 | ); 228 | buildRules = ( 229 | ); 230 | dependencies = ( 231 | 72D193F01CC3576A00645F83 /* PBXTargetDependency */, 232 | ); 233 | name = StackViewControllerTests; 234 | productName = StackViewControllerTests; 235 | productReference = 72D193ED1CC3576A00645F83 /* StackViewControllerTests.xctest */; 236 | productType = "com.apple.product-type.bundle.unit-test"; 237 | }; 238 | 72D1940A1CC357A100645F83 /* Example */ = { 239 | isa = PBXNativeTarget; 240 | buildConfigurationList = 72D1941A1CC357A100645F83 /* Build configuration list for PBXNativeTarget "Example" */; 241 | buildPhases = ( 242 | 72D194071CC357A100645F83 /* Sources */, 243 | 72D194081CC357A100645F83 /* Frameworks */, 244 | 72D194091CC357A100645F83 /* Resources */, 245 | 72D194211CC357A800645F83 /* Embed Frameworks */, 246 | ); 247 | buildRules = ( 248 | ); 249 | dependencies = ( 250 | 72D194201CC357A800645F83 /* PBXTargetDependency */, 251 | ); 252 | name = Example; 253 | productName = Example; 254 | productReference = 72D1940B1CC357A100645F83 /* Example.app */; 255 | productType = "com.apple.product-type.application"; 256 | }; 257 | /* End PBXNativeTarget section */ 258 | 259 | /* Begin PBXProject section */ 260 | 72B21E4D1CBC34EF00724EB8 /* Project object */ = { 261 | isa = PBXProject; 262 | attributes = { 263 | LastSwiftUpdateCheck = 0730; 264 | LastUpgradeCheck = 1300; 265 | ORGANIZATIONNAME = "Seed Platform, Inc"; 266 | TargetAttributes = { 267 | 72D193E31CC3576A00645F83 = { 268 | CreatedOnToolsVersion = 7.3; 269 | LastSwiftMigration = 1110; 270 | }; 271 | 72D193EC1CC3576A00645F83 = { 272 | CreatedOnToolsVersion = 7.3; 273 | LastSwiftMigration = 1110; 274 | }; 275 | 72D1940A1CC357A100645F83 = { 276 | CreatedOnToolsVersion = 7.3; 277 | LastSwiftMigration = 1110; 278 | }; 279 | }; 280 | }; 281 | buildConfigurationList = 72B21E501CBC34EF00724EB8 /* Build configuration list for PBXProject "StackViewController" */; 282 | compatibilityVersion = "Xcode 3.2"; 283 | developmentRegion = en; 284 | hasScannedForEncodings = 0; 285 | knownRegions = ( 286 | en, 287 | Base, 288 | ); 289 | mainGroup = 72B21E4C1CBC34EF00724EB8; 290 | productRefGroup = 72B21E561CBC34EF00724EB8 /* Products */; 291 | projectDirPath = ""; 292 | projectRoot = ""; 293 | targets = ( 294 | 72D193E31CC3576A00645F83 /* StackViewController */, 295 | 72D193EC1CC3576A00645F83 /* StackViewControllerTests */, 296 | 72D1940A1CC357A100645F83 /* Example */, 297 | ); 298 | }; 299 | /* End PBXProject section */ 300 | 301 | /* Begin PBXResourcesBuildPhase section */ 302 | 72D193E21CC3576A00645F83 /* Resources */ = { 303 | isa = PBXResourcesBuildPhase; 304 | buildActionMask = 2147483647; 305 | files = ( 306 | ); 307 | runOnlyForDeploymentPostprocessing = 0; 308 | }; 309 | 72D193EB1CC3576A00645F83 /* Resources */ = { 310 | isa = PBXResourcesBuildPhase; 311 | buildActionMask = 2147483647; 312 | files = ( 313 | ); 314 | runOnlyForDeploymentPostprocessing = 0; 315 | }; 316 | 72D194091CC357A100645F83 /* Resources */ = { 317 | isa = PBXResourcesBuildPhase; 318 | buildActionMask = 2147483647; 319 | files = ( 320 | 72D194181CC357A100645F83 /* LaunchScreen.storyboard in Resources */, 321 | 72D194151CC357A100645F83 /* Assets.xcassets in Resources */, 322 | ); 323 | runOnlyForDeploymentPostprocessing = 0; 324 | }; 325 | /* End PBXResourcesBuildPhase section */ 326 | 327 | /* Begin PBXSourcesBuildPhase section */ 328 | 72D193DF1CC3576A00645F83 /* Sources */ = { 329 | isa = PBXSourcesBuildPhase; 330 | buildActionMask = 2147483647; 331 | files = ( 332 | 756AE907270B9DA000644AC5 /* ContentWidth.swift in Sources */, 333 | 72DE37211CCDABA8009CAE32 /* UIViewExtensions.swift in Sources */, 334 | 72D194031CC3577B00645F83 /* SeparatorView.swift in Sources */, 335 | 72DE37291CCDCCB6009CAE32 /* UIStackViewExtensions.swift in Sources */, 336 | 72D194061CC3577B00645F83 /* StackViewItem.swift in Sources */, 337 | 72D194051CC3577B00645F83 /* StackViewController.swift in Sources */, 338 | 72D194011CC3577B00645F83 /* AutoScrollView.swift in Sources */, 339 | 72D194041CC3577B00645F83 /* StackViewContainer.swift in Sources */, 340 | ); 341 | runOnlyForDeploymentPostprocessing = 0; 342 | }; 343 | 72D193E91CC3576A00645F83 /* Sources */ = { 344 | isa = PBXSourcesBuildPhase; 345 | buildActionMask = 2147483647; 346 | files = ( 347 | 72D193F31CC3576A00645F83 /* StackViewControllerTests.swift in Sources */, 348 | 72D194231CC35CDC00645F83 /* StackViewContainerTests.swift in Sources */, 349 | ); 350 | runOnlyForDeploymentPostprocessing = 0; 351 | }; 352 | 72D194071CC357A100645F83 /* Sources */ = { 353 | isa = PBXSourcesBuildPhase; 354 | buildActionMask = 2147483647; 355 | files = ( 356 | 72D194101CC357A100645F83 /* ViewController.swift in Sources */, 357 | 72D1940E1CC357A100645F83 /* AppDelegate.swift in Sources */, 358 | 72DE372B1CCDD998009CAE32 /* ImageThumbnailView.swift in Sources */, 359 | 72DE37231CCDB9AE009CAE32 /* LabeledTextFieldController.swift in Sources */, 360 | 72DE37271CCDC769009CAE32 /* ImageAttachmentViewController.swift in Sources */, 361 | 72DE37251CCDC75D009CAE32 /* ImageAttachmentView.swift in Sources */, 362 | 72DE371F1CCDA961009CAE32 /* LabeledTextField.swift in Sources */, 363 | ); 364 | runOnlyForDeploymentPostprocessing = 0; 365 | }; 366 | /* End PBXSourcesBuildPhase section */ 367 | 368 | /* Begin PBXTargetDependency section */ 369 | 72D193F01CC3576A00645F83 /* PBXTargetDependency */ = { 370 | isa = PBXTargetDependency; 371 | target = 72D193E31CC3576A00645F83 /* StackViewController */; 372 | targetProxy = 72D193EF1CC3576A00645F83 /* PBXContainerItemProxy */; 373 | }; 374 | 72D194201CC357A800645F83 /* PBXTargetDependency */ = { 375 | isa = PBXTargetDependency; 376 | target = 72D193E31CC3576A00645F83 /* StackViewController */; 377 | targetProxy = 72D1941F1CC357A800645F83 /* PBXContainerItemProxy */; 378 | }; 379 | /* End PBXTargetDependency section */ 380 | 381 | /* Begin PBXVariantGroup section */ 382 | 72D194161CC357A100645F83 /* LaunchScreen.storyboard */ = { 383 | isa = PBXVariantGroup; 384 | children = ( 385 | 72D194171CC357A100645F83 /* Base */, 386 | ); 387 | name = LaunchScreen.storyboard; 388 | sourceTree = ""; 389 | }; 390 | /* End PBXVariantGroup section */ 391 | 392 | /* Begin XCBuildConfiguration section */ 393 | 72B21E651CBC34F000724EB8 /* Debug */ = { 394 | isa = XCBuildConfiguration; 395 | buildSettings = { 396 | ALWAYS_SEARCH_USER_PATHS = NO; 397 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 398 | CLANG_ANALYZER_NONNULL = YES; 399 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 400 | CLANG_CXX_LIBRARY = "libc++"; 401 | CLANG_ENABLE_MODULES = YES; 402 | CLANG_ENABLE_OBJC_ARC = YES; 403 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 404 | CLANG_WARN_BOOL_CONVERSION = YES; 405 | CLANG_WARN_COMMA = YES; 406 | CLANG_WARN_CONSTANT_CONVERSION = YES; 407 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 408 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 409 | CLANG_WARN_EMPTY_BODY = YES; 410 | CLANG_WARN_ENUM_CONVERSION = YES; 411 | CLANG_WARN_INFINITE_RECURSION = YES; 412 | CLANG_WARN_INT_CONVERSION = YES; 413 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 414 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 415 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 416 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 417 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 418 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 419 | CLANG_WARN_STRICT_PROTOTYPES = YES; 420 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 421 | CLANG_WARN_UNREACHABLE_CODE = YES; 422 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 423 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 424 | COPY_PHASE_STRIP = NO; 425 | DEBUG_INFORMATION_FORMAT = dwarf; 426 | ENABLE_STRICT_OBJC_MSGSEND = YES; 427 | ENABLE_TESTABILITY = YES; 428 | GCC_C_LANGUAGE_STANDARD = gnu99; 429 | GCC_DYNAMIC_NO_PIC = NO; 430 | GCC_NO_COMMON_BLOCKS = YES; 431 | GCC_OPTIMIZATION_LEVEL = 0; 432 | GCC_PREPROCESSOR_DEFINITIONS = ( 433 | "DEBUG=1", 434 | "$(inherited)", 435 | ); 436 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 437 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 438 | GCC_WARN_UNDECLARED_SELECTOR = YES; 439 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 440 | GCC_WARN_UNUSED_FUNCTION = YES; 441 | GCC_WARN_UNUSED_VARIABLE = YES; 442 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 443 | MTL_ENABLE_DEBUG_INFO = YES; 444 | ONLY_ACTIVE_ARCH = YES; 445 | SDKROOT = iphoneos; 446 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 447 | }; 448 | name = Debug; 449 | }; 450 | 72B21E661CBC34F000724EB8 /* Release */ = { 451 | isa = XCBuildConfiguration; 452 | buildSettings = { 453 | ALWAYS_SEARCH_USER_PATHS = NO; 454 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 455 | CLANG_ANALYZER_NONNULL = YES; 456 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 457 | CLANG_CXX_LIBRARY = "libc++"; 458 | CLANG_ENABLE_MODULES = YES; 459 | CLANG_ENABLE_OBJC_ARC = YES; 460 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 461 | CLANG_WARN_BOOL_CONVERSION = YES; 462 | CLANG_WARN_COMMA = YES; 463 | CLANG_WARN_CONSTANT_CONVERSION = YES; 464 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 465 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 466 | CLANG_WARN_EMPTY_BODY = YES; 467 | CLANG_WARN_ENUM_CONVERSION = YES; 468 | CLANG_WARN_INFINITE_RECURSION = YES; 469 | CLANG_WARN_INT_CONVERSION = YES; 470 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 471 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 472 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 473 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 474 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 475 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 476 | CLANG_WARN_STRICT_PROTOTYPES = YES; 477 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 478 | CLANG_WARN_UNREACHABLE_CODE = YES; 479 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 480 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 481 | COPY_PHASE_STRIP = NO; 482 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 483 | ENABLE_NS_ASSERTIONS = NO; 484 | ENABLE_STRICT_OBJC_MSGSEND = YES; 485 | GCC_C_LANGUAGE_STANDARD = gnu99; 486 | GCC_NO_COMMON_BLOCKS = YES; 487 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 488 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 489 | GCC_WARN_UNDECLARED_SELECTOR = YES; 490 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 491 | GCC_WARN_UNUSED_FUNCTION = YES; 492 | GCC_WARN_UNUSED_VARIABLE = YES; 493 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 494 | MTL_ENABLE_DEBUG_INFO = NO; 495 | SDKROOT = iphoneos; 496 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 497 | VALIDATE_PRODUCT = YES; 498 | }; 499 | name = Release; 500 | }; 501 | 72D193F61CC3576A00645F83 /* Debug */ = { 502 | isa = XCBuildConfiguration; 503 | buildSettings = { 504 | CLANG_ENABLE_MODULES = YES; 505 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 506 | CURRENT_PROJECT_VERSION = 1; 507 | DEFINES_MODULE = YES; 508 | DYLIB_COMPATIBILITY_VERSION = 1; 509 | DYLIB_CURRENT_VERSION = 1; 510 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 511 | INFOPLIST_FILE = StackViewController/Info.plist; 512 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 513 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 514 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 515 | PRODUCT_BUNDLE_IDENTIFIER = co.seed.StackViewController; 516 | PRODUCT_NAME = "$(TARGET_NAME)"; 517 | SKIP_INSTALL = YES; 518 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 519 | SWIFT_VERSION = 5.0; 520 | TARGETED_DEVICE_FAMILY = "1,2"; 521 | VERSIONING_SYSTEM = "apple-generic"; 522 | VERSION_INFO_PREFIX = ""; 523 | }; 524 | name = Debug; 525 | }; 526 | 72D193F71CC3576A00645F83 /* Release */ = { 527 | isa = XCBuildConfiguration; 528 | buildSettings = { 529 | CLANG_ENABLE_MODULES = YES; 530 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 531 | CURRENT_PROJECT_VERSION = 1; 532 | DEFINES_MODULE = YES; 533 | DYLIB_COMPATIBILITY_VERSION = 1; 534 | DYLIB_CURRENT_VERSION = 1; 535 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 536 | INFOPLIST_FILE = StackViewController/Info.plist; 537 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 538 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 539 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 540 | PRODUCT_BUNDLE_IDENTIFIER = co.seed.StackViewController; 541 | PRODUCT_NAME = "$(TARGET_NAME)"; 542 | SKIP_INSTALL = YES; 543 | SWIFT_VERSION = 5.0; 544 | TARGETED_DEVICE_FAMILY = "1,2"; 545 | VERSIONING_SYSTEM = "apple-generic"; 546 | VERSION_INFO_PREFIX = ""; 547 | }; 548 | name = Release; 549 | }; 550 | 72D193F91CC3576A00645F83 /* Debug */ = { 551 | isa = XCBuildConfiguration; 552 | buildSettings = { 553 | INFOPLIST_FILE = StackViewControllerTests/Info.plist; 554 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 555 | PRODUCT_BUNDLE_IDENTIFIER = co.seed.StackViewControllerTests; 556 | PRODUCT_NAME = "$(TARGET_NAME)"; 557 | SWIFT_VERSION = 5.0; 558 | }; 559 | name = Debug; 560 | }; 561 | 72D193FA1CC3576A00645F83 /* Release */ = { 562 | isa = XCBuildConfiguration; 563 | buildSettings = { 564 | INFOPLIST_FILE = StackViewControllerTests/Info.plist; 565 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 566 | PRODUCT_BUNDLE_IDENTIFIER = co.seed.StackViewControllerTests; 567 | PRODUCT_NAME = "$(TARGET_NAME)"; 568 | SWIFT_VERSION = 5.0; 569 | }; 570 | name = Release; 571 | }; 572 | 72D1941B1CC357A100645F83 /* Debug */ = { 573 | isa = XCBuildConfiguration; 574 | buildSettings = { 575 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 576 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 577 | INFOPLIST_FILE = Example/Info.plist; 578 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 579 | PRODUCT_BUNDLE_IDENTIFIER = co.seed.Example; 580 | PRODUCT_NAME = "$(TARGET_NAME)"; 581 | SWIFT_VERSION = 5.0; 582 | TARGETED_DEVICE_FAMILY = "1,2"; 583 | }; 584 | name = Debug; 585 | }; 586 | 72D1941C1CC357A100645F83 /* Release */ = { 587 | isa = XCBuildConfiguration; 588 | buildSettings = { 589 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 590 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 591 | INFOPLIST_FILE = Example/Info.plist; 592 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 593 | PRODUCT_BUNDLE_IDENTIFIER = co.seed.Example; 594 | PRODUCT_NAME = "$(TARGET_NAME)"; 595 | SWIFT_VERSION = 5.0; 596 | TARGETED_DEVICE_FAMILY = "1,2"; 597 | }; 598 | name = Release; 599 | }; 600 | /* End XCBuildConfiguration section */ 601 | 602 | /* Begin XCConfigurationList section */ 603 | 72B21E501CBC34EF00724EB8 /* Build configuration list for PBXProject "StackViewController" */ = { 604 | isa = XCConfigurationList; 605 | buildConfigurations = ( 606 | 72B21E651CBC34F000724EB8 /* Debug */, 607 | 72B21E661CBC34F000724EB8 /* Release */, 608 | ); 609 | defaultConfigurationIsVisible = 0; 610 | defaultConfigurationName = Release; 611 | }; 612 | 72D193F51CC3576A00645F83 /* Build configuration list for PBXNativeTarget "StackViewController" */ = { 613 | isa = XCConfigurationList; 614 | buildConfigurations = ( 615 | 72D193F61CC3576A00645F83 /* Debug */, 616 | 72D193F71CC3576A00645F83 /* Release */, 617 | ); 618 | defaultConfigurationIsVisible = 0; 619 | defaultConfigurationName = Release; 620 | }; 621 | 72D193F81CC3576A00645F83 /* Build configuration list for PBXNativeTarget "StackViewControllerTests" */ = { 622 | isa = XCConfigurationList; 623 | buildConfigurations = ( 624 | 72D193F91CC3576A00645F83 /* Debug */, 625 | 72D193FA1CC3576A00645F83 /* Release */, 626 | ); 627 | defaultConfigurationIsVisible = 0; 628 | defaultConfigurationName = Release; 629 | }; 630 | 72D1941A1CC357A100645F83 /* Build configuration list for PBXNativeTarget "Example" */ = { 631 | isa = XCConfigurationList; 632 | buildConfigurations = ( 633 | 72D1941B1CC357A100645F83 /* Debug */, 634 | 72D1941C1CC357A100645F83 /* Release */, 635 | ); 636 | defaultConfigurationIsVisible = 0; 637 | defaultConfigurationName = Release; 638 | }; 639 | /* End XCConfigurationList section */ 640 | }; 641 | rootObject = 72B21E4D1CBC34EF00724EB8 /* Project object */; 642 | } 643 | -------------------------------------------------------------------------------- /StackViewController.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /StackViewController.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /StackViewController.xcodeproj/xcshareddata/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /StackViewController.xcodeproj/xcshareddata/xcschemes/StackViewController.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /StackViewController/AutoScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoScrollView.swift 3 | // Seed 4 | // 5 | // Created by Indragie Karunaratne on 2016-03-10. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A scroll view that automatically scrolls to a subview of its `contentView` 12 | /// when the keyboard is shown. This replicates the behaviour implemented by 13 | /// `UITableView`. 14 | open class AutoScrollView: UIScrollView { 15 | fileprivate struct Constants { 16 | static let DefaultAnimationDuration: TimeInterval = 0.25 17 | static let DefaultAnimationCurve = UIView.AnimationCurve.easeInOut 18 | static let ScrollAnimationID = "AutoscrollAnimation" 19 | } 20 | 21 | /// The content view to display inside the container view. Views can also 22 | /// be added directly to this view without using the `contentView` property, 23 | /// but it simply makes it more convenient for the common case where your 24 | /// content fills the bounds of the scroll view. 25 | open var contentView: UIView? { 26 | willSet { 27 | contentView?.removeFromSuperview() 28 | } 29 | didSet { 30 | if let contentView = contentView { 31 | contentView.translatesAutoresizingMaskIntoConstraints = false 32 | addSubview(contentView) 33 | updateContentViewConstraints() 34 | } 35 | } 36 | } 37 | fileprivate var contentViewConstraints: [NSLayoutConstraint]? 38 | 39 | /// This layout guide follows the `contentView` and honors the `horizontallyCompactContentWidth` 40 | /// and `horizontallyRegularContentWidth` values. 41 | public var contentViewLayoutGuide = UILayoutGuide() 42 | private var contentViewLayoutGuideConstraints: [NSLayoutConstraint] = [] 43 | 44 | /// This setting determines how the content should be laid out in a horizontally compact environment. 45 | public var horizontallyCompactContentWidth: ContentWidth = .matchScrollViewWidth { 46 | didSet { 47 | guard horizontallyCompactContentWidth != oldValue else { return } 48 | 49 | switch traitCollection.horizontalSizeClass { 50 | case .compact: updateContentViewConstraints() 51 | case .regular, .unspecified: break // no-op 52 | @unknown default: break 53 | } 54 | } 55 | } 56 | 57 | /// This setting determines how the content should be laid out in a horizontally regular environment. 58 | public var horizontallyRegularContentWidth: ContentWidth = .matchScrollViewWidth { 59 | didSet { 60 | guard horizontallyRegularContentWidth != oldValue else { return } 61 | 62 | switch traitCollection.horizontalSizeClass { 63 | case .regular: updateContentViewConstraints() 64 | case .compact, .unspecified: break // no-op 65 | @unknown default: break 66 | } 67 | } 68 | } 69 | 70 | override open var contentInset: UIEdgeInsets { 71 | didSet { 72 | updateContentViewConstraints() 73 | } 74 | } 75 | 76 | fileprivate func updateContentViewConstraints() { 77 | let contentViewFollowsReadableWidth: Bool 78 | switch traitCollection.horizontalSizeClass { 79 | case .compact: 80 | switch horizontallyCompactContentWidth { 81 | case .matchScrollViewWidth: 82 | contentViewFollowsReadableWidth = false 83 | case .matchReadableContentGuideWidth: 84 | contentViewFollowsReadableWidth = true 85 | } 86 | 87 | case .regular: 88 | switch horizontallyRegularContentWidth { 89 | case .matchScrollViewWidth: 90 | contentViewFollowsReadableWidth = false 91 | case .matchReadableContentGuideWidth: 92 | contentViewFollowsReadableWidth = true 93 | } 94 | 95 | case .unspecified: 96 | return // abort early 97 | 98 | @unknown default: 99 | contentViewFollowsReadableWidth = false 100 | } 101 | 102 | if let constraints = contentViewConstraints { 103 | NSLayoutConstraint.deactivate(constraints) 104 | } 105 | NSLayoutConstraint.deactivate(contentViewLayoutGuideConstraints) 106 | 107 | let newLayoutGuide: UILayoutGuide 108 | if contentViewFollowsReadableWidth, let contentView = contentView { 109 | let newConstraints = [ 110 | contentView.leadingAnchor.constraint( 111 | equalTo: readableContentGuide.leadingAnchor, 112 | constant: contentInset.left 113 | ), 114 | contentView.trailingAnchor.constraint( 115 | equalTo: readableContentGuide.trailingAnchor, 116 | constant: -contentInset.right 117 | ), 118 | contentView.topAnchor.constraint(equalTo: topAnchor, constant: contentInset.top), 119 | contentView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -contentInset.bottom), 120 | ] 121 | NSLayoutConstraint.activate(newConstraints) 122 | contentViewConstraints = newConstraints 123 | 124 | newLayoutGuide = readableContentGuide 125 | } else { 126 | contentViewConstraints = contentView?.activateSuperviewHuggingConstraints(insets: contentInset) 127 | 128 | newLayoutGuide = frameLayoutGuide 129 | } 130 | 131 | contentViewLayoutGuideConstraints = [ 132 | contentViewLayoutGuide.leadingAnchor.constraint(equalTo: newLayoutGuide.leadingAnchor), 133 | contentViewLayoutGuide.topAnchor.constraint(equalTo: newLayoutGuide.topAnchor), 134 | contentViewLayoutGuide.trailingAnchor.constraint(equalTo: newLayoutGuide.trailingAnchor), 135 | contentViewLayoutGuide.bottomAnchor.constraint(equalTo: newLayoutGuide.bottomAnchor), 136 | ] 137 | NSLayoutConstraint.activate(contentViewLayoutGuideConstraints) 138 | } 139 | 140 | fileprivate func commonInit() { 141 | let nc = NotificationCenter.default 142 | nc.addObserver(self, selector: #selector(AutoScrollView.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) 143 | nc.addObserver(self, selector: #selector(AutoScrollView.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) 144 | 145 | addLayoutGuide(contentViewLayoutGuide) 146 | } 147 | 148 | public override init(frame: CGRect) { 149 | super.init(frame: frame) 150 | commonInit() 151 | } 152 | 153 | public required init?(coder aDecoder: NSCoder) { 154 | super.init(coder: aDecoder) 155 | commonInit() 156 | } 157 | 158 | deinit { 159 | NotificationCenter.default.removeObserver(self) 160 | } 161 | 162 | open override func touchesShouldCancel(in view: UIView) -> Bool { 163 | guard view.isKind(of: UIButton.self) else { return super.touchesShouldCancel(in: view) } 164 | return true 165 | } 166 | 167 | open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 168 | guard traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass else { 169 | return 170 | } 171 | 172 | updateContentViewConstraints() 173 | } 174 | 175 | // MARK: Notifications 176 | 177 | // Implementation based on code from Apple documentation 178 | // https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html 179 | @objc fileprivate func keyboardWillShow(_ notification: Notification) { 180 | let keyboardFrameValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue 181 | guard var keyboardFrame = keyboardFrameValue?.cgRectValue else { return } 182 | keyboardFrame = convert(keyboardFrame, from: nil) 183 | 184 | let bottomInset: CGFloat 185 | let keyboardIntersectionRect = bounds.intersection(keyboardFrame) 186 | if !keyboardIntersectionRect.isNull { 187 | bottomInset = keyboardIntersectionRect.height 188 | let contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0) 189 | super.contentInset = contentInset 190 | scrollIndicatorInsets = contentInset 191 | } else { 192 | bottomInset = 0.0 193 | } 194 | 195 | guard let firstResponder = firstResponder else { return } 196 | let firstResponderFrame = firstResponder.convert(firstResponder.bounds, to: self) 197 | 198 | var contentBounds = CGRect(origin: contentOffset, size: bounds.size) 199 | contentBounds.size.height -= bottomInset 200 | if !contentBounds.contains(firstResponderFrame.origin) { 201 | let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? Constants.DefaultAnimationDuration 202 | let curve = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? UIView.AnimationCurve ?? Constants.DefaultAnimationCurve 203 | 204 | // Dropping down to the old style UIView animation API because the new API 205 | // does not support setting the curve directly. The other option is to take 206 | // `curve` and shift it left by 16 bits to turn it into a `UIViewAnimationOptions`, 207 | // but that seems uglier than just doing this. 208 | UIView.beginAnimations(Constants.ScrollAnimationID, context: nil) 209 | UIView.setAnimationCurve(curve) 210 | UIView.setAnimationDuration(duration) 211 | scrollRectToVisible(firstResponderFrame, animated: false) 212 | UIView.commitAnimations() 213 | } 214 | } 215 | 216 | @objc fileprivate func keyboardWillHide(_ notification: Notification) { 217 | super.contentInset = UIEdgeInsets.zero 218 | scrollIndicatorInsets = UIEdgeInsets.zero 219 | } 220 | } 221 | 222 | private extension UIView { 223 | var firstResponder: UIView? { 224 | if isFirstResponder { 225 | return self 226 | } 227 | for subview in subviews { 228 | if let responder = subview.firstResponder { 229 | return responder 230 | } 231 | } 232 | return nil 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /StackViewController/ContentWidth.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentWidth.swift 3 | // StackViewController 4 | // 5 | // Created by Devin McKaskle on 10/4/21. 6 | // Copyright © 2021 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | @objc public enum ContentWidth: Int { 10 | case matchScrollViewWidth 11 | case matchReadableContentGuideWidth 12 | 13 | public mutating func toggle() { 14 | self = ContentWidth(rawValue: rawValue + 1) ?? .matchScrollViewWidth 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /StackViewController/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /StackViewController/SeparatorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SeparatorView.swift 3 | // StackViewController 4 | // 5 | // Created by Indragie Karunaratne on 2016-04-12. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A customizable separator view that can be displayed in horizontal and 12 | /// vertical orientations. 13 | open class SeparatorView: UIView { 14 | fileprivate var sizeConstraint: NSLayoutConstraint? 15 | 16 | /// The thickness of the separator. This is equivalent to the height for 17 | /// a horizontal separator and the width for a vertical separator. 18 | open var separatorThickness: CGFloat = 1.0 { 19 | didSet { 20 | sizeConstraint?.constant = separatorThickness 21 | setNeedsDisplay() 22 | } 23 | } 24 | 25 | /// The inset of the separator from the left (MinX) edge for a horizontal 26 | /// separator and from the bottom (MaxY) edge for a vertical separator. 27 | open var separatorInset: CGFloat = 15.0 { 28 | didSet { setNeedsDisplay() } 29 | } 30 | 31 | /// The color of the separator 32 | open var separatorColor = UIColor(white: 0.90, alpha: 1.0) { 33 | didSet { setNeedsDisplay() } 34 | } 35 | 36 | /// The axis (horizontal or vertical) of the separator 37 | open var axis = NSLayoutConstraint.Axis.horizontal { 38 | didSet { updateSizeConstraint() } 39 | } 40 | 41 | /// Initializes the receiver for display on the specified axis. 42 | public init(axis: NSLayoutConstraint.Axis) { 43 | self.axis = axis 44 | super.init(frame: CGRect.zero) 45 | commonInit() 46 | } 47 | 48 | required public init?(coder aDecoder: NSCoder) { 49 | super.init(coder: aDecoder) 50 | commonInit() 51 | } 52 | 53 | fileprivate func updateSizeConstraint() { 54 | sizeConstraint?.isActive = false 55 | let layoutAttribute: NSLayoutConstraint.Attribute = { 56 | switch axis { 57 | case .horizontal: return .height 58 | case .vertical: return .width 59 | @unknown default: 60 | assertionFailure("Unknown axis. Assuming it should behave the same as vertical.") 61 | return .width 62 | } 63 | }() 64 | sizeConstraint = NSLayoutConstraint( 65 | item: self, 66 | attribute: layoutAttribute, 67 | relatedBy: .equal, 68 | toItem: nil, attribute: 69 | .notAnAttribute, 70 | multiplier: 1.0, 71 | constant: separatorThickness 72 | ) 73 | sizeConstraint?.isActive = true 74 | } 75 | 76 | fileprivate func commonInit() { 77 | backgroundColor = .clear 78 | updateSizeConstraint() 79 | } 80 | 81 | open override func draw(_ rect: CGRect) { 82 | guard separatorThickness > 0 else { return } 83 | let edge: CGRectEdge = { 84 | switch axis { 85 | case .horizontal: return .minXEdge 86 | case .vertical: return .maxYEdge 87 | @unknown default: 88 | assertionFailure("Unknown axis. Assuming it should behave the same as vertical.") 89 | return .maxYEdge 90 | } 91 | }() 92 | let (_, separatorRect) = bounds.divided(atDistance: separatorInset, from: edge) 93 | separatorColor.setFill() 94 | UIRectFillUsingBlendMode(separatorRect, .normal) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /StackViewController/StackViewContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StackViewContainer.swift 3 | // StackViewController 4 | // 5 | // Created by Indragie Karunaratne on 2016-04-11. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A container for a `UIStackView` that adds some additional capabilities, including 12 | /// being able to change the background color, assigning a background view, and 13 | /// using view controller composition to display content. 14 | open class StackViewContainer: UIView, UIScrollViewDelegate { 15 | /// The scroll view that is the superview of the stack view. 16 | public let scrollView: AutoScrollView 17 | 18 | /// The stack view. It is not safe to modify the arranged subviews directly 19 | /// via the stack view. The content view collection accessors on 20 | /// `StackViewContainer` should be used instead. It is also not safe to modify 21 | /// the `axis` property. `StackViewContainer.axis` should be set instead. 22 | public let stackView: UIStackView 23 | 24 | fileprivate let backgroundColorContainerView = UIView(frame: CGRect.zero) 25 | 26 | /// An optional background view that is shown behind the stack view. The 27 | /// top of the background view will be kept pinned to the top of the scroll 28 | /// view bounds, even when bouncing. 29 | open var backgroundView: UIView? { 30 | get { return _backgroundView } 31 | set { 32 | backgroundViewTopConstraint = nil 33 | _backgroundView?.removeFromSuperview() 34 | _backgroundView = newValue 35 | layoutBackgroundView() 36 | } 37 | } 38 | fileprivate var _backgroundView: UIView? 39 | fileprivate var backgroundViewTopConstraint: NSLayoutConstraint? 40 | 41 | /// The content views that are displayed inside the stack view. This array 42 | /// does not include separator views that are automatically inserted by 43 | /// the container if the `separatorViewFactory` property is set. 44 | /// 45 | /// Setting this array causes all of the existing content views in the 46 | /// stack view to be removed and replaced with the new content views. 47 | open var contentViews: [UIView] { 48 | get { return _contentViews } 49 | set { 50 | _contentViews = newValue 51 | relayoutContent(true) 52 | } 53 | } 54 | fileprivate var _contentViews = [UIView]() 55 | 56 | fileprivate var items = [Item]() 57 | open var separatorViewFactory: SeparatorViewFactory? { 58 | didSet { relayoutContent(false) } 59 | } 60 | 61 | /// Creates a separator view factory that uses the `SeparatorView` class 62 | /// provided by this framework to render the view. The separator will 63 | /// automatically use the correct orientation based on the orientation 64 | /// of the stack view. The `configurator` block can be used to customize 65 | /// the appearance of the separator. 66 | public static func createSeparatorViewFactory(_ configurator: ((SeparatorView) -> Void)? = nil) -> SeparatorViewFactory { 67 | return { axis in 68 | let separatorAxis: NSLayoutConstraint.Axis = { 69 | switch axis { 70 | case .horizontal: return .vertical 71 | case .vertical: return .horizontal 72 | @unknown default: 73 | assertionFailure("Unknown axis. Assuming it should behave the same as vertical.") 74 | return .horizontal 75 | } 76 | }() 77 | let separatorView = SeparatorView(axis: separatorAxis) 78 | configurator?(separatorView) 79 | return separatorView 80 | } 81 | } 82 | 83 | /// The axis (direction) that content is laid out in. Setting the axis via 84 | /// this property instead of `stackView.axis` ensures that any separator 85 | /// views are recreated to account for the change in layout direction. 86 | open var axis: NSLayoutConstraint.Axis { 87 | get { return stackView.axis } 88 | set { 89 | stackView.axis = newValue 90 | updateSizeConstraint() 91 | relayoutContent(false) 92 | } 93 | } 94 | fileprivate var stackViewSizeConstraint: NSLayoutConstraint? 95 | 96 | open override var backgroundColor: UIColor? { 97 | didSet { 98 | scrollView.backgroundColor = backgroundColor 99 | backgroundColorContainerView.backgroundColor = backgroundColor 100 | } 101 | } 102 | 103 | public typealias SeparatorViewFactory = (NSLayoutConstraint.Axis) -> UIView 104 | 105 | /// Initializes an instance of `StackViewContainer` using a stack view 106 | /// with the default configuration, which is simply a `UIStackView` with 107 | /// all of its properties set to the default values except for `axis`, which 108 | /// is set to `.Vertical`. 109 | public convenience init() { 110 | self.init(stackView: constructDefaultStackView()) 111 | } 112 | 113 | /// Initializes an instance of `StackViewContainer` using an existing 114 | /// instance of `UIStackView`. Any existing arranged subviews of the stack 115 | /// view are removed prior to `StackViewContainer` taking ownership of it. 116 | public init(stackView: UIStackView) { 117 | stackView.removeAllArrangedSubviews() 118 | self.stackView = stackView 119 | self.scrollView = AutoScrollView(frame: CGRect.zero) 120 | super.init(frame: CGRect.zero) 121 | commonInit() 122 | } 123 | 124 | required public init?(coder aDecoder: NSCoder) { 125 | stackView = constructDefaultStackView() 126 | scrollView = AutoScrollView(frame: CGRect.zero) 127 | super.init(coder: aDecoder) 128 | commonInit() 129 | } 130 | 131 | fileprivate func commonInit() { 132 | backgroundColorContainerView.addSubview(stackView) 133 | _ = stackView.activateSuperviewHuggingConstraints() 134 | scrollView.contentView = backgroundColorContainerView 135 | scrollView.delegate = self 136 | addSubview(scrollView) 137 | _ = scrollView.activateSuperviewHuggingConstraints() 138 | updateSizeConstraint() 139 | } 140 | 141 | fileprivate func updateSizeConstraint() { 142 | stackViewSizeConstraint?.isActive = false 143 | let attribute: NSLayoutConstraint.Attribute = { 144 | switch axis { 145 | case .horizontal: return .height 146 | case .vertical: return .width 147 | @unknown default: 148 | assertionFailure("Unknown axis. Assuming it should behave the same as vertical.") 149 | return .width 150 | } 151 | }() 152 | stackViewSizeConstraint = 153 | NSLayoutConstraint(item: stackView, attribute: attribute, relatedBy: .equal, toItem: scrollView.contentViewLayoutGuide, attribute: attribute, multiplier: 1.0, constant: 0.0) 154 | stackViewSizeConstraint?.isActive = true 155 | } 156 | 157 | fileprivate func layoutBackgroundView() { 158 | guard let backgroundView = _backgroundView else { return } 159 | scrollView.insertSubview(backgroundView, at: 0) 160 | 161 | let constraints = backgroundView.activateSuperviewHuggingConstraints() 162 | for constraint in constraints { 163 | if constraint.firstAttribute == .top { 164 | backgroundViewTopConstraint = constraint 165 | break 166 | } 167 | } 168 | } 169 | 170 | // MARK: Managing Content 171 | 172 | /** 173 | Adds a content view to the list of content views that this container 174 | manages. 175 | 176 | - parameter view: The content view to add 177 | - parameter canShowSeparator: See the documentation for 178 | `StackViewContainer.setCanShowSeparator(:forContentViewAtIndex:)` for more 179 | details on this parameter. 180 | */ 181 | open func addContentView(_ view: UIView, canShowSeparator: Bool = true) { 182 | insertContentView(view, atIndex: items.endIndex, canShowSeparator: canShowSeparator) 183 | } 184 | 185 | /** 186 | Inserts a content view into the list of content views that this container 187 | manages. 188 | 189 | - parameter view: The content view to insert 190 | - parameter index: The index to insert the content view at, in 191 | the `contentViews` array 192 | - parameter canShowSeparator: See the documentation for 193 | `StackViewContainer.setCanShowSeparator(:forContentViewAtIndex:)` for more 194 | details on this parameter. 195 | */ 196 | open func insertContentView(_ view: UIView, atIndex index: Int, canShowSeparator: Bool = true) { 197 | precondition(index >= items.startIndex) 198 | precondition(index <= items.endIndex) 199 | 200 | let stackInsertionIndex: Int 201 | if items.isEmpty { 202 | stackInsertionIndex = 0 203 | } else { 204 | let lastExistingIndex = (items.endIndex - 1) 205 | let lastItem = items[lastExistingIndex] 206 | if index == lastExistingIndex { 207 | // If a content view is inserted at (items.count - 1), the last 208 | // content item will become the final item in the list, in which 209 | // case its separator should be removed. 210 | if let separatorView = lastItem.separatorView { 211 | stackView.removeArrangedSubview(separatorView) 212 | lastItem.separatorView = nil 213 | } 214 | stackInsertionIndex = indexOfArrangedSubview(lastItem.contentView) 215 | } else if index == items.endIndex { 216 | // If a content view is being inserted at the end of the list, the 217 | // item before it should have a separator added. 218 | if lastItem.separatorView == nil && lastItem.canShowSeparator { 219 | if let separatorView = createSeparatorView() { 220 | lastItem.separatorView = separatorView 221 | stackView.addArrangedSubview(separatorView) 222 | } 223 | } 224 | stackInsertionIndex = stackView.arrangedSubviews.endIndex 225 | } else { 226 | stackInsertionIndex = indexOfArrangedSubview(items[index].contentView) 227 | } 228 | } 229 | 230 | let separatorView: UIView? 231 | // Only show the separator if the item is not the last item in the list 232 | if canShowSeparator && index < items.endIndex { 233 | separatorView = createSeparatorView() 234 | } else { 235 | separatorView = nil 236 | } 237 | 238 | let item = Item( 239 | contentView: view, 240 | canShowSeparator: canShowSeparator, 241 | separatorView: separatorView 242 | ) 243 | items.insert(item, at: index) 244 | _contentViews.insert(view, at: index) 245 | stackView.insertArrangedSubview(view, at: stackInsertionIndex) 246 | if let separatorView = separatorView { 247 | stackView.insertArrangedSubview(separatorView, at: (stackInsertionIndex + 1)) 248 | } 249 | } 250 | 251 | fileprivate func indexOfArrangedSubview(_ subview: UIView) -> Int { 252 | if let index = stackView.arrangedSubviews.firstIndex(where: { $0 === subview }) { 253 | return index 254 | } else { 255 | fatalError("Called indexOfArrangedSubview with subview that doesn't exist in stackView.arrangedSubviews") 256 | } 257 | } 258 | 259 | /** 260 | Removes a content view from the list of content views managed by this container. 261 | If `view` does not exist in `contentViews`, this method does nothing. 262 | 263 | - parameter view: The content view to remove 264 | */ 265 | open func removeContentView(_ view: UIView) { 266 | guard let index = _contentViews.firstIndex(where: { $0 === view }) else { return } 267 | removeContentViewAtIndex(index) 268 | } 269 | 270 | /** 271 | Removes a content view from the list of content views managed by this container. 272 | 273 | - parameter index: The index of the content view to remove 274 | */ 275 | open func removeContentViewAtIndex(_ index: Int) { 276 | precondition(index >= items.startIndex) 277 | precondition(index < items.endIndex) 278 | 279 | let item = items[index] 280 | if items.count >= 1 && index == (items.endIndex - 1) && index > 0 { 281 | let previousItem = items[(index - 1)] 282 | if let separatorView = previousItem.separatorView { 283 | stackView.removeArrangedSubview(separatorView) 284 | previousItem.separatorView = nil 285 | } 286 | } 287 | stackView.removeArrangedSubview(item.contentView) 288 | if let separatorView = item.separatorView { 289 | stackView.removeArrangedSubview(separatorView) 290 | } 291 | items.remove(at: index) 292 | let view = _contentViews.remove(at: index) 293 | view.removeFromSuperview() 294 | } 295 | 296 | /** 297 | Controls the visibility of the separator view that comes after a content view. 298 | If `view` does not exist in `contentViews`, this method does nothing. 299 | 300 | - parameter canShowSeparator: See the documentation for 301 | `StackViewContainer.setCanShowSeparator(:forContentViewAtIndex:)` for more 302 | details on this parameter. 303 | - parameter view: The content view for which to set separator 304 | visibility. 305 | */ 306 | open func setCanShowSeparator(_ canShowSeparator: Bool, forContentView view: UIView) { 307 | guard let index = _contentViews.firstIndex(where: { $0 === view }) else { return } 308 | setCanShowSeparator(canShowSeparator, forContentViewAtIndex: index) 309 | } 310 | 311 | /** 312 | Controls the visibility of the separator view that comes after a content view. 313 | 314 | - parameter canShowSeparator: Whether it is possible for the content view 315 | to show a separator view *after* it (i.e. to the right of the content view 316 | if the stack view orientation is horizontal, and to the bottom of the 317 | content view if the stack view orientation is vertical). A separator will 318 | not be shown if the content view is the last content view in the list. 319 | - parameter index: The index of the content view for which to 320 | set separator visibility. 321 | */ 322 | open func setCanShowSeparator(_ canShowSeparator: Bool, forContentViewAtIndex index: Int) { 323 | let item = items[index] 324 | if canShowSeparator 325 | && (index < (items.endIndex - 1)) 326 | && item.separatorView == nil { 327 | if let separatorView = createSeparatorView() { 328 | item.separatorView = separatorView 329 | stackView.insertArrangedSubview(separatorView, at: (index + 1)) 330 | } 331 | } else if let separatorView = item.separatorView, !canShowSeparator { 332 | stackView.removeArrangedSubview(separatorView) 333 | item.separatorView = nil 334 | } 335 | } 336 | 337 | fileprivate func relayoutContent(_ didUpdateContent: Bool) { 338 | let canShowSeparatorConfig: [Bool]? 339 | if didUpdateContent { 340 | canShowSeparatorConfig = nil 341 | } else { 342 | canShowSeparatorConfig = items.map { $0.canShowSeparator } 343 | } 344 | let canShowSeparator: ((Int) -> Bool) = { index in 345 | if let canShowSeparatorConfig = canShowSeparatorConfig { 346 | return canShowSeparatorConfig[index] 347 | } else { 348 | return true 349 | } 350 | } 351 | items.removeAll(keepingCapacity: true) 352 | stackView.removeAllArrangedSubviews() 353 | let contentViews = _contentViews 354 | _contentViews.removeAll(keepingCapacity: true) 355 | for (index, contentView) in contentViews.enumerated() { 356 | addContentView(contentView, canShowSeparator: canShowSeparator(index)) 357 | } 358 | } 359 | 360 | fileprivate func createSeparatorView() -> UIView? { 361 | guard let separatorViewFactory = separatorViewFactory else { return nil } 362 | return separatorViewFactory(stackView.axis) 363 | } 364 | 365 | // MARK: UIScrollViewDelegate 366 | 367 | open func scrollViewDidScroll(_ scrollView: UIScrollView) { 368 | guard let backgroundViewTopConstraint = backgroundViewTopConstraint else { return } 369 | backgroundViewTopConstraint.constant = -max(-scrollView.contentOffset.y, 0) 370 | } 371 | 372 | // MARK: ContentContainerView 373 | 374 | fileprivate class Item { 375 | fileprivate let contentView: UIView 376 | fileprivate let canShowSeparator: Bool 377 | fileprivate var separatorView: UIView? 378 | 379 | init(contentView: UIView, canShowSeparator: Bool, separatorView: UIView?) { 380 | self.contentView = contentView 381 | self.canShowSeparator = canShowSeparator 382 | self.separatorView = separatorView 383 | } 384 | } 385 | } 386 | 387 | private func constructDefaultStackView() -> UIStackView { 388 | let stackView = UIStackView(frame: CGRect.zero) 389 | stackView.axis = .vertical 390 | return stackView 391 | } 392 | -------------------------------------------------------------------------------- /StackViewController/StackViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // StackViewController.h 3 | // StackViewController 4 | // 5 | // Created by Indragie Karunaratne on 2016-04-16. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for StackViewController. 12 | FOUNDATION_EXPORT double StackViewControllerVersionNumber; 13 | 14 | //! Project version string for StackViewController. 15 | FOUNDATION_EXPORT const unsigned char StackViewControllerVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /StackViewController/StackViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StackViewController.swift 3 | // StackViewController 4 | // 5 | // Created by Indragie Karunaratne on 2016-04-11. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// Provides a view controller composition based API on top of the 12 | /// `StackViewContainer` API. Instead of adding content views directly to a view, 13 | /// view controllers that control each content view are added as child view 14 | /// controllers via the API exposed in this class. Adding and removing these 15 | /// child view controllers is managed automatically. 16 | open class StackViewController: UIViewController { 17 | /// An optional background view that is shown behind the stack view. The 18 | /// top of the background view will be kept pinned to the top of the scroll 19 | /// view bounds, even when bouncing. 20 | open var backgroundView: UIView? { 21 | get { 22 | return stackViewContainer.backgroundView 23 | } 24 | set { 25 | stackViewContainer.backgroundView = newValue 26 | } 27 | } 28 | 29 | open var backgroundColor: UIColor? { 30 | get { 31 | return stackViewContainer.backgroundColor 32 | } 33 | set { 34 | stackViewContainer.backgroundColor = newValue 35 | } 36 | } 37 | 38 | /// The stack view. It is not safe to modify the arranged subviews directly 39 | /// via the stack view. The items collection accessors on 40 | /// `StackViewController` should be used instead. It is also not safe to modify 41 | /// the `axis` property. `StackViewController.axis` should be set instead. 42 | open var stackView: UIStackView { 43 | return stackViewContainer.stackView 44 | } 45 | 46 | /// The axis (direction) that content is laid out in. Setting the axis via 47 | /// this property instead of `stackView.axis` ensures that any separator 48 | /// views are recreated to account for the change in layout direction. 49 | open var axis: NSLayoutConstraint.Axis { 50 | get { 51 | return stackViewContainer.axis 52 | } 53 | set { 54 | stackViewContainer.axis = newValue 55 | } 56 | } 57 | 58 | open var separatorViewFactory: StackViewContainer.SeparatorViewFactory? { 59 | get { 60 | return stackViewContainer.separatorViewFactory 61 | } 62 | set { 63 | stackViewContainer.separatorViewFactory = newValue 64 | } 65 | } 66 | 67 | /// The scroll view that is the superview of the stack view. 68 | /// The scrollview automatically accommodates the keyboard. This replicates the behaviour 69 | /// implemented by `UITableView`. 70 | open var scrollView: UIScrollView { 71 | return stackViewContainer.scrollView 72 | } 73 | 74 | /// This setting determines how the content should be laid out in a horizontally compact environment. 75 | public var horizontallyCompactContentWidth: ContentWidth { 76 | get { stackViewContainer.scrollView.horizontallyCompactContentWidth } 77 | set { stackViewContainer.scrollView.horizontallyCompactContentWidth = newValue } 78 | } 79 | 80 | /// This setting determines how the content should be laid out in a horizontally regular environment. 81 | public var horizontallyRegularContentWidth: ContentWidth { 82 | get { stackViewContainer.scrollView.horizontallyRegularContentWidth } 83 | set { stackViewContainer.scrollView.horizontallyRegularContentWidth = newValue } 84 | } 85 | 86 | private lazy var stackViewContainer = StackViewContainer() 87 | 88 | fileprivate var _items = [StackViewItem]() 89 | 90 | /// The items displayed by this controller 91 | open var items: [StackViewItem] { 92 | get { return _items } 93 | set(newItems) { 94 | for index in _items.indices.reversed() { 95 | removeItemAtIndex(index) 96 | } 97 | for item in newItems { 98 | addItem(item, canShowSeparator: true) 99 | } 100 | } 101 | } 102 | fileprivate var viewControllers = [UIViewController]() 103 | 104 | open override func loadView() { 105 | view = stackViewContainer 106 | } 107 | 108 | /** 109 | Adds an item to the list of items managed by the controller. The item can 110 | be either a `UIView` or a `UIViewController`, both of which conform to the 111 | `StackViewItem` protocol. 112 | 113 | - parameter item: The item to add 114 | - parameter canShowSeparator: See the documentation for 115 | `StackViewContainer.setCanShowSeparator(:forContentViewAtIndex:)` for more 116 | details on this parameter. 117 | */ 118 | open func addItem(_ item: StackViewItem, canShowSeparator: Bool = true) { 119 | insertItem(item, atIndex: _items.endIndex, canShowSeparator: canShowSeparator) 120 | } 121 | 122 | /** 123 | Inserts an item into the list of items managed by the controller. The item can 124 | be either a `UIView` or a `UIViewController`, both of which conform to the 125 | `StackViewItem` protocol. 126 | 127 | - parameter item: The item to insert 128 | - parameter index: The index to insert the item at 129 | - parameter canShowSeparator: See the documentation for 130 | `StackViewContainer.setCanShowSeparator(:forContentViewAtIndex:)` for more 131 | details on this parameter. 132 | */ 133 | open func insertItem(_ item: StackViewItem, atIndex index: Int, canShowSeparator: Bool = true) { 134 | precondition(index >= _items.startIndex) 135 | precondition(index <= _items.endIndex) 136 | 137 | _items.insert(item, at: index) 138 | let viewController = item.toViewController() 139 | viewControllers.insert(viewController, at: index) 140 | addChild(viewController) 141 | stackViewContainer.insertContentView(viewController.view, atIndex: index, canShowSeparator: canShowSeparator) 142 | } 143 | 144 | /** 145 | Removes an item from the list of items managed by this controller. If `item` 146 | does not exist in `items`, this method does nothing. 147 | 148 | - parameter item: The item to remove. 149 | */ 150 | open func removeItem(_ item: StackViewItem) { 151 | 152 | guard let index = _items.firstIndex(where: { $0 === item }) else { return } 153 | removeItemAtIndex(index) 154 | } 155 | 156 | /** 157 | Removes an item from the list of items managed by this controller. 158 | 159 | - parameter index: The index of the item to remove 160 | */ 161 | open func removeItemAtIndex(_ index: Int) { 162 | _items.remove(at: index) 163 | let viewController = viewControllers[index] 164 | viewController.willMove(toParent: nil) 165 | stackViewContainer.removeContentViewAtIndex(index) 166 | viewController.removeFromParent() 167 | viewControllers.remove(at: index) 168 | } 169 | 170 | /** 171 | Sets whether a separator can be shown for `item` 172 | 173 | - parameter canShowSeparator: See the documentation for 174 | `StackViewContainer.setCanShowSeparator(:forContentViewAtIndex:)` for more 175 | details on this parameter. 176 | - parameter item: The item for which to configure separator 177 | visibility 178 | */ 179 | open func setCanShowSeparator(_ canShowSeparator: Bool, forItem item: StackViewItem) { 180 | guard let index = _items.firstIndex(where: { $0 === item }) else { return } 181 | setCanShowSeparator(canShowSeparator, forItemAtIndex: index) 182 | } 183 | 184 | /** 185 | Sets whether a separator can be shown for the item at index `index` 186 | 187 | - parameter canShowSeparator: See the documentation for 188 | `StackViewContainer.setCanShowSeparator(:forContentViewAtIndex:)` for more 189 | details on this parameter. 190 | - parameter index: The index of the item to configure separator 191 | visibility for. 192 | */ 193 | open func setCanShowSeparator(_ canShowSeparator: Bool, forItemAtIndex index: Int) { 194 | stackViewContainer.setCanShowSeparator(canShowSeparator, forContentViewAtIndex: index) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /StackViewController/StackViewItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerConvertible.swift 3 | // Seed 4 | // 5 | // Created by Indragie Karunaratne on 1/29/16. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol StackViewItem: AnyObject { 12 | func toViewController() -> UIViewController 13 | } 14 | 15 | extension UIViewController: StackViewItem { 16 | public func toViewController() -> UIViewController { 17 | return self 18 | } 19 | } 20 | 21 | extension UIView: StackViewItem { 22 | public func toViewController() -> UIViewController { 23 | return WrapperViewController(view: self) 24 | } 25 | } 26 | 27 | private class WrapperViewController: UIViewController { 28 | fileprivate let _view: UIView 29 | 30 | init(view: UIView) { 31 | _view = view 32 | super.init(nibName: nil, bundle: nil) 33 | } 34 | 35 | required init?(coder aDecoder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | fileprivate override func loadView() { 40 | view = _view 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /StackViewController/UIStackViewExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStackViewExtensions.swift 3 | // StackViewController 4 | // 5 | // Created by Indragie Karunaratne on 2016-04-24. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension UIStackView { 12 | @objc func removeAllArrangedSubviews() { 13 | arrangedSubviews.forEach { 14 | $0.removeFromSuperview() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /StackViewController/UIViewExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewExtensions.swift 3 | // StackViewController 4 | // 5 | // Created by Indragie Karunaratne on 2016-04-24. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension UIView { 12 | @objc func activateSuperviewHuggingConstraints(insets: UIEdgeInsets = UIEdgeInsets.zero) -> [NSLayoutConstraint] { 13 | translatesAutoresizingMaskIntoConstraints = false 14 | let views = ["view": self] 15 | let metrics = ["top": insets.top, "left": insets.left, "bottom": insets.bottom, "right": insets.right] 16 | var constraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-left-[view]-right-|", options: [], metrics: metrics, views: views) 17 | constraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-top-[view]-bottom-|", options: [], metrics: metrics, views: views)) 18 | NSLayoutConstraint.activate(constraints) 19 | return constraints 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /StackViewControllerTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /StackViewControllerTests/StackViewContainerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StackViewContainerTests.swift 3 | // StackViewController 4 | // 5 | // Created by Indragie Karunaratne on 2016-04-16. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import StackViewController 11 | 12 | class StackViewContainerTests: XCTestCase { 13 | fileprivate final class ContentView: UIView {} 14 | 15 | fileprivate var stackViewContainer: StackViewContainer! 16 | 17 | override func setUp() { 18 | super.setUp() 19 | stackViewContainer = StackViewContainer() 20 | stackViewContainer.separatorViewFactory = StackViewContainer.createSeparatorViewFactory() 21 | } 22 | 23 | fileprivate func contentViewWithTag(_ tag: Int) -> ContentView { 24 | let contentView = ContentView() 25 | contentView.tag = tag 26 | return contentView 27 | } 28 | 29 | func testStackInitiallyEmpty() { 30 | XCTAssertEqual(0, stackViewContainer.stackView.arrangedSubviews.count) 31 | XCTAssertEqual(0, stackViewContainer.contentViews.count) 32 | } 33 | 34 | func testAddContentViewWithoutSeparator() { 35 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: false) 36 | stackViewContainer.addContentView(contentViewWithTag(2), canShowSeparator: true) 37 | 38 | XCTAssertEqual(2, stackViewContainer.stackView.arrangedSubviews.count) 39 | XCTAssertEqual([1, 2], stackViewContainer.contentViews.map { $0.tag }) 40 | } 41 | 42 | func testAddContentViewWithSeparator() { 43 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: true) 44 | stackViewContainer.addContentView(contentViewWithTag(2), canShowSeparator: true) 45 | 46 | XCTAssertEqual(2, stackViewContainer.contentViews.count) 47 | XCTAssertEqual([1, 0, 2], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 48 | } 49 | 50 | func testInsertContentViewAtLastExistingIndexWithSeparator() { 51 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: true) 52 | stackViewContainer.addContentView(contentViewWithTag(2), canShowSeparator: true) 53 | stackViewContainer.insertContentView(contentViewWithTag(3), atIndex: 1, canShowSeparator: true) 54 | 55 | XCTAssertEqual(3, stackViewContainer.contentViews.count) 56 | XCTAssertEqual([1, 0, 3, 0, 2], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 57 | } 58 | 59 | func testInsertContentViewAtLastExistingIndexWithoutSeparator() { 60 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: true) 61 | stackViewContainer.addContentView(contentViewWithTag(2), canShowSeparator: true) 62 | stackViewContainer.insertContentView(contentViewWithTag(3), atIndex: 1, canShowSeparator: false) 63 | 64 | XCTAssertEqual(3, stackViewContainer.contentViews.count) 65 | XCTAssertEqual([1, 0, 3, 2], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 66 | } 67 | 68 | func testInsertContentViewAtEndIndexWithSeparator() { 69 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: true) 70 | stackViewContainer.insertContentView(contentViewWithTag(2), atIndex: 1, canShowSeparator: true) 71 | 72 | XCTAssertEqual(2, stackViewContainer.contentViews.count) 73 | XCTAssertEqual([1, 0, 2], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 74 | } 75 | 76 | func testInsertContentViewAtEndIndexWithoutSeparator() { 77 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: true) 78 | stackViewContainer.insertContentView(contentViewWithTag(2), atIndex: 1, canShowSeparator: false) 79 | 80 | XCTAssertEqual(2, stackViewContainer.contentViews.count) 81 | XCTAssertEqual([1, 0, 2], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 82 | } 83 | 84 | func testInsertContentViewAtIndexWithSeparator() { 85 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: true) 86 | stackViewContainer.addContentView(contentViewWithTag(2), canShowSeparator: true) 87 | stackViewContainer.addContentView(contentViewWithTag(3), canShowSeparator: true) 88 | stackViewContainer.addContentView(contentViewWithTag(4), canShowSeparator: true) 89 | 90 | stackViewContainer.insertContentView(contentViewWithTag(5), atIndex: 2, canShowSeparator: true) 91 | 92 | XCTAssertEqual(5, stackViewContainer.contentViews.count) 93 | XCTAssertEqual([1, 0, 2, 0, 5, 0, 3, 0, 4], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 94 | } 95 | 96 | func testInsertContentViewAtIndexWithoutSeparator() { 97 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: true) 98 | stackViewContainer.addContentView(contentViewWithTag(2), canShowSeparator: true) 99 | stackViewContainer.addContentView(contentViewWithTag(3), canShowSeparator: true) 100 | stackViewContainer.addContentView(contentViewWithTag(4), canShowSeparator: true) 101 | 102 | stackViewContainer.insertContentView(contentViewWithTag(5), atIndex: 2, canShowSeparator: false) 103 | 104 | XCTAssertEqual(5, stackViewContainer.contentViews.count) 105 | XCTAssertEqual([1, 0, 2, 0, 5, 3, 0, 4], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 106 | } 107 | 108 | func testRemoveContentViewAtLastExistingIndexWithSeparator() { 109 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: true) 110 | stackViewContainer.addContentView(contentViewWithTag(2), canShowSeparator: true) 111 | 112 | stackViewContainer.removeContentViewAtIndex(1) 113 | 114 | XCTAssertEqual(1, stackViewContainer.contentViews.count) 115 | XCTAssertEqual([1], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 116 | } 117 | 118 | func testRemoveContentViewAtLastExistingIndexWithoutSeparator() { 119 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: true) 120 | stackViewContainer.addContentView(contentViewWithTag(2), canShowSeparator: false) 121 | 122 | stackViewContainer.removeContentViewAtIndex(1) 123 | 124 | XCTAssertEqual(1, stackViewContainer.contentViews.count) 125 | XCTAssertEqual([1], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 126 | } 127 | 128 | func testRemoveContentViewAtIndexWithSeparator() { 129 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: true) 130 | stackViewContainer.addContentView(contentViewWithTag(2), canShowSeparator: true) 131 | stackViewContainer.addContentView(contentViewWithTag(3), canShowSeparator: true) 132 | 133 | stackViewContainer.removeContentViewAtIndex(0) 134 | 135 | XCTAssertEqual(2, stackViewContainer.contentViews.count) 136 | XCTAssertEqual([2, 0, 3], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 137 | } 138 | 139 | func testRemoveContentViewAtIndexWithoutSeparator() { 140 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: false) 141 | stackViewContainer.addContentView(contentViewWithTag(2), canShowSeparator: true) 142 | stackViewContainer.addContentView(contentViewWithTag(3), canShowSeparator: true) 143 | 144 | stackViewContainer.removeContentViewAtIndex(0) 145 | 146 | XCTAssertEqual(2, stackViewContainer.contentViews.count) 147 | XCTAssertEqual([2, 0, 3], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 148 | } 149 | 150 | func testRemoveContentViewWithExistingView() { 151 | let contentView = contentViewWithTag(1) 152 | stackViewContainer.addContentView(contentView, canShowSeparator: true) 153 | stackViewContainer.addContentView(contentViewWithTag(2), canShowSeparator: true) 154 | stackViewContainer.addContentView(contentViewWithTag(3), canShowSeparator: true) 155 | 156 | stackViewContainer.removeContentView(contentView) 157 | 158 | XCTAssertEqual(2, stackViewContainer.contentViews.count) 159 | XCTAssertEqual([2, 0, 3], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 160 | } 161 | 162 | func testRemoveContentViewWithNonexistentView() { 163 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: true) 164 | stackViewContainer.addContentView(contentViewWithTag(2), canShowSeparator: true) 165 | stackViewContainer.addContentView(contentViewWithTag(3), canShowSeparator: true) 166 | 167 | stackViewContainer.removeContentView(contentViewWithTag(3)) 168 | 169 | XCTAssertEqual(3, stackViewContainer.contentViews.count) 170 | XCTAssertEqual([1, 0, 2, 0, 3], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 171 | } 172 | 173 | func testSetCanShowSeparatorTrue() { 174 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: false) 175 | stackViewContainer.addContentView(contentViewWithTag(2), canShowSeparator: true) 176 | 177 | stackViewContainer.setCanShowSeparator(true, forContentViewAtIndex: 0) 178 | 179 | XCTAssertEqual(2, stackViewContainer.contentViews.count) 180 | XCTAssertEqual([1, 0, 2], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 181 | } 182 | 183 | func testSetCanShowSeparatorFalse() { 184 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: true) 185 | stackViewContainer.addContentView(contentViewWithTag(2), canShowSeparator: true) 186 | 187 | stackViewContainer.setCanShowSeparator(false, forContentViewAtIndex: 0) 188 | 189 | XCTAssertEqual(2, stackViewContainer.contentViews.count) 190 | XCTAssertEqual([1, 2], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 191 | } 192 | 193 | func testSetCanShowSeparatorTrueWithLastExistingIndex() { 194 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: false) 195 | 196 | stackViewContainer.setCanShowSeparator(true, forContentViewAtIndex: 0) 197 | 198 | XCTAssertEqual(1, stackViewContainer.contentViews.count) 199 | XCTAssertEqual([1], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 200 | } 201 | 202 | func testSetCanShowSeparatorFalseWithLastExistingIndex() { 203 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: true) 204 | 205 | stackViewContainer.setCanShowSeparator(false, forContentViewAtIndex: 0) 206 | 207 | XCTAssertEqual(1, stackViewContainer.contentViews.count) 208 | XCTAssertEqual([1], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 209 | } 210 | 211 | func testSetCanShowSeparatorWithExistingView() { 212 | let contentView = contentViewWithTag(1) 213 | stackViewContainer.addContentView(contentView, canShowSeparator: false) 214 | stackViewContainer.addContentView(contentViewWithTag(2), canShowSeparator: true) 215 | 216 | stackViewContainer.setCanShowSeparator(true, forContentView: contentView) 217 | 218 | XCTAssertEqual(2, stackViewContainer.contentViews.count) 219 | XCTAssertEqual([1, 0, 2], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 220 | } 221 | 222 | func testSetCanShowSeparatorWithNonexistentView() { 223 | stackViewContainer.addContentView(contentViewWithTag(1), canShowSeparator: true) 224 | stackViewContainer.addContentView(contentViewWithTag(2), canShowSeparator: true) 225 | 226 | stackViewContainer.setCanShowSeparator(false, forContentView: contentViewWithTag(1)) 227 | 228 | XCTAssertEqual(2, stackViewContainer.contentViews.count) 229 | XCTAssertEqual([1, 0, 2], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 230 | } 231 | 232 | func testContentViewsSetter() { 233 | stackViewContainer.contentViews = [ 234 | contentViewWithTag(1), 235 | contentViewWithTag(2) 236 | ] 237 | XCTAssertEqual(2, stackViewContainer.contentViews.count) 238 | XCTAssertEqual([1, 0, 2], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 239 | 240 | stackViewContainer.contentViews = [] 241 | XCTAssertEqual(0, stackViewContainer.contentViews.count) 242 | XCTAssertEqual([], stackViewContainer.stackView.arrangedSubviews) 243 | } 244 | 245 | func testSeparatorViewFactorySetter() { 246 | let separatorViewFactory = stackViewContainer.separatorViewFactory 247 | stackViewContainer.separatorViewFactory = nil 248 | stackViewContainer.contentViews = [ 249 | contentViewWithTag(1), 250 | contentViewWithTag(2) 251 | ] 252 | XCTAssertEqual(2, stackViewContainer.contentViews.count) 253 | XCTAssertEqual([1, 2], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 254 | 255 | stackViewContainer.separatorViewFactory = separatorViewFactory 256 | XCTAssertEqual(2, stackViewContainer.contentViews.count) 257 | XCTAssertEqual([1, 0, 2], stackViewContainer.stackView.arrangedSubviews.map { $0.tag }) 258 | } 259 | 260 | func testAxisSetter() { 261 | stackViewContainer.contentViews = [ 262 | contentViewWithTag(1), 263 | contentViewWithTag(2) 264 | ] 265 | 266 | let assertSeparatorAxes: (NSLayoutConstraint.Axis) -> Void = { axis in 267 | let separators: [SeparatorView] = self.stackViewContainer.stackView.arrangedSubviews 268 | .filter { $0.isKind(of: SeparatorView.self) } 269 | .map { $0 as! SeparatorView } 270 | separators.forEach { 271 | XCTAssertNotEqual(axis, $0.axis) 272 | } 273 | } 274 | 275 | assertSeparatorAxes(.vertical) 276 | stackViewContainer.axis = .horizontal 277 | assertSeparatorAxes(.horizontal) 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /StackViewControllerTests/StackViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StackViewControllerTests.swift 3 | // StackViewControllerTests 4 | // 5 | // Created by Indragie Karunaratne on 2016-04-16. 6 | // Copyright © 2016 Seed Platform, Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import StackViewController 11 | 12 | class StackViewControllerTests: XCTestCase { 13 | fileprivate var stackViewController: StackViewController! 14 | 15 | fileprivate class TestViewController: UIViewController { 16 | fileprivate let tag: Int 17 | 18 | init(tag: Int) { 19 | self.tag = tag 20 | super.init(nibName: nil, bundle: nil) 21 | } 22 | 23 | required init?(coder aDecoder: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | fileprivate override func loadView() { 28 | view = UIView(frame: CGRect.zero) 29 | view.tag = tag 30 | } 31 | } 32 | 33 | override func setUp() { 34 | super.setUp() 35 | stackViewController = StackViewController() 36 | } 37 | 38 | fileprivate func createViewWithTag(_ tag: Int) -> UIView { 39 | let view = UIView(frame: CGRect.zero) 40 | view.tag = tag 41 | return view 42 | } 43 | 44 | func testAddView() { 45 | stackViewController.addItem(createViewWithTag(1)) 46 | 47 | XCTAssertEqual(1, stackViewController.items[0].tag) 48 | XCTAssertEqual(1, stackViewController.children.count) 49 | } 50 | 51 | func testAddViewController() { 52 | let viewController = TestViewController(tag: 10) 53 | stackViewController.addItem(viewController) 54 | 55 | XCTAssertEqual(10, stackViewController.items[0].tag) 56 | XCTAssertEqual(1, stackViewController.children.count) 57 | XCTAssertEqual(viewController, stackViewController.children[0]) 58 | } 59 | 60 | func testInsertView() { 61 | stackViewController.addItem(TestViewController(tag: 10)) 62 | stackViewController.addItem(TestViewController(tag: 10)) 63 | stackViewController.insertItem(createViewWithTag(1), atIndex: 1) 64 | 65 | XCTAssertEqual([10, 1, 10], stackViewController.items.map { $0.tag }) 66 | XCTAssertEqual(3, stackViewController.children.count) 67 | } 68 | 69 | func testInsertViewController() { 70 | stackViewController.addItem(createViewWithTag(1)) 71 | stackViewController.addItem(createViewWithTag(2)) 72 | stackViewController.insertItem(TestViewController(tag: 10), atIndex: 1) 73 | 74 | XCTAssertEqual([1, 10, 2], stackViewController.items.map { $0.tag }) 75 | XCTAssertEqual(3, stackViewController.children.count) 76 | } 77 | 78 | func testRemoveViewAtIndex() { 79 | stackViewController.addItem(createViewWithTag(1)) 80 | stackViewController.addItem(TestViewController(tag: 10)) 81 | stackViewController.removeItemAtIndex(0) 82 | 83 | XCTAssertEqual([10], stackViewController.items.map { $0.tag }) 84 | XCTAssertEqual(1, stackViewController.children.count) 85 | } 86 | 87 | func testRemoveViewControllerAtIndex() { 88 | stackViewController.addItem(createViewWithTag(1)) 89 | stackViewController.addItem(TestViewController(tag: 10)) 90 | stackViewController.removeItemAtIndex(1) 91 | 92 | XCTAssertEqual([1], stackViewController.items.map { $0.tag }) 93 | XCTAssertEqual(1, stackViewController.children.count) 94 | } 95 | 96 | func testRemoveItemWithExistingItem() { 97 | stackViewController.addItem(createViewWithTag(1)) 98 | let viewController = TestViewController(tag: 10) 99 | stackViewController.addItem(viewController) 100 | stackViewController.removeItem(viewController) 101 | 102 | XCTAssertEqual([1], stackViewController.items.map { $0.tag }) 103 | XCTAssertEqual(1, stackViewController.children.count) 104 | } 105 | 106 | func testRemoveItemWithNonexistentItem() { 107 | stackViewController.addItem(createViewWithTag(1)) 108 | stackViewController.addItem(TestViewController(tag: 10)) 109 | stackViewController.removeItem(TestViewController(tag: 10)) 110 | 111 | XCTAssertEqual([1, 10], stackViewController.items.map { $0.tag }) 112 | XCTAssertEqual(2, stackViewController.children.count) 113 | } 114 | 115 | func testItemsSetter() { 116 | stackViewController.items = [ 117 | createViewWithTag(1), 118 | TestViewController(tag: 10) 119 | ] 120 | 121 | XCTAssertEqual([1, 10], stackViewController.items.map { $0.tag }) 122 | XCTAssertEqual(2, stackViewController.children.count) 123 | } 124 | 125 | func testReplaceItems() { 126 | stackViewController.items = [ 127 | createViewWithTag(1), 128 | TestViewController(tag: 10) 129 | ] 130 | 131 | stackViewController.items = [ 132 | createViewWithTag(2), 133 | TestViewController(tag: 20) 134 | ] 135 | 136 | XCTAssertEqual([2, 20], stackViewController.items.map { $0.tag }) 137 | XCTAssertEqual(2, stackViewController.children.count) 138 | } 139 | } 140 | 141 | private extension StackViewItem { 142 | var tag: Int { 143 | if let view = self as? UIView { 144 | return view.tag 145 | } else if let viewController = self as? UIViewController { 146 | return viewController.view.tag 147 | } else { 148 | return -1 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seedco/StackViewController/b88e7a855ff6a08739a7b99a2825f059bd916152/screenshot.png --------------------------------------------------------------------------------