├── .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 [](https://github.com/Carthage/Carthage) [](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 |
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
--------------------------------------------------------------------------------