├── screenshot.png
├── Example
├── Assets.xcassets
│ ├── Contents.json
│ ├── attach-button.imageset
│ │ ├── attach-button.png
│ │ ├── attach-button@2x.png
│ │ ├── attach-button@3x.png
│ │ └── Contents.json
│ ├── delete-button.imageset
│ │ ├── delete-button.png
│ │ ├── delete-button@2x.png
│ │ ├── delete-button@3x.png
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── AppDelegate.swift
├── LabeledTextFieldController.swift
├── Info.plist
├── Base.lproj
│ └── LaunchScreen.storyboard
├── LabeledTextField.swift
├── ImageAttachmentView.swift
├── ImageThumbnailView.swift
├── ViewController.swift
└── ImageAttachmentViewController.swift
├── circle.yml
├── StackViewController.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
├── xcshareddata
│ └── xcschemes
│ │ └── StackViewController.xcscheme
└── project.pbxproj
├── StackViewController
├── UIStackViewExtensions.swift
├── StackViewController.h
├── Info.plist
├── UIViewExtensions.swift
├── StackViewItem.swift
├── SeparatorView.swift
├── StackViewController.swift
├── AutoScrollView.swift
└── StackViewContainer.swift
├── Makefile
├── StackViewControllerTests
├── Info.plist
├── StackViewControllerTests.swift
└── StackViewContainerTests.swift
├── SeedStackViewController.podspec
├── LICENSE.md
├── .gitignore
└── README.md
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabrielPeart/StackViewController/HEAD/screenshot.png
--------------------------------------------------------------------------------
/Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/Assets.xcassets/attach-button.imageset/attach-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabrielPeart/StackViewController/HEAD/Example/Assets.xcassets/attach-button.imageset/attach-button.png
--------------------------------------------------------------------------------
/Example/Assets.xcassets/delete-button.imageset/delete-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabrielPeart/StackViewController/HEAD/Example/Assets.xcassets/delete-button.imageset/delete-button.png
--------------------------------------------------------------------------------
/Example/Assets.xcassets/attach-button.imageset/attach-button@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabrielPeart/StackViewController/HEAD/Example/Assets.xcassets/attach-button.imageset/attach-button@2x.png
--------------------------------------------------------------------------------
/Example/Assets.xcassets/attach-button.imageset/attach-button@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabrielPeart/StackViewController/HEAD/Example/Assets.xcassets/attach-button.imageset/attach-button@3x.png
--------------------------------------------------------------------------------
/Example/Assets.xcassets/delete-button.imageset/delete-button@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabrielPeart/StackViewController/HEAD/Example/Assets.xcassets/delete-button.imageset/delete-button@2x.png
--------------------------------------------------------------------------------
/Example/Assets.xcassets/delete-button.imageset/delete-button@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabrielPeart/StackViewController/HEAD/Example/Assets.xcassets/delete-button.imageset/delete-button@3x.png
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | xcode:
3 | version: 7.3
4 |
5 | dependencies:
6 | override:
7 | - sudo -H gem install xcpretty
8 |
9 | test:
10 | override:
11 | - make clean test
12 |
--------------------------------------------------------------------------------
/StackViewController.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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 | extension UIStackView {
12 | public func removeAllArrangedSubviews() {
13 | arrangedSubviews.forEach {
14 | $0.removeFromSuperview()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | SDK="iphonesimulator9.3"
3 | DESTINATION="platform=iOS Simulator,name=iPhone 6,OS=9.3"
4 | PROJECT="StackViewController"
5 |
6 | .PHONY: all
7 |
8 | build:
9 | set -o pipefail && \
10 | xcodebuild \
11 | -sdk $(SDK) \
12 | -derivedDataPath build \
13 | -project $(PROJECT).xcodeproj \
14 | -scheme $(PROJECT) \
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 $(PROJECT) \
26 | -configuration Debug \
27 | -destination $(DESTINATION) \
28 | test | xcpretty
29 |
30 |
31 | clean:
32 | rm -rf build
33 |
34 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/SeedStackViewController.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = "SeedStackViewController"
3 | s.version = "0.1.0"
4 | s.summary = "Simplifies the process of building forms and other static content using UIStackView."
5 | s.description = <<-DESC
6 | StackViewController is a Swift framework that simplifies the process of building forms and other static content using UIStackView.
7 | DESC
8 |
9 | s.homepage = "https://github.com/seedco/StackViewController"
10 | s.license = 'MIT'
11 | s.author = { "Indragie Karunaratne" => "i@indragie.com" }
12 | s.source = { :git => "https://github.com/seedco/StackViewController.git", :tag => s.version.to_s }
13 | s.ios.deployment_target = '9.0'
14 | s.source_files = 'StackViewController/**'
15 | s.frameworks = 'UIKit'
16 | end
17 |
--------------------------------------------------------------------------------
/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: [NSObject: AnyObject]?) -> Bool {
16 | self.window = {
17 | let window = UIWindow(frame: UIScreen.mainScreen().bounds)
18 | window.backgroundColor = .whiteColor()
19 | window.rootViewController = UINavigationController(rootViewController: ViewController())
20 | window.makeKeyAndVisible()
21 | return window
22 | }()
23 | return true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/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/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 | extension UIView {
12 | public func activateSuperviewHuggingConstraints(insets insets: UIEdgeInsets = UIEdgeInsetsZero) -> [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.constraintsWithVisualFormat("H:|-left-[view]-right-|", options: [], metrics: metrics, views: views)
17 | constraints.appendContentsOf(NSLayoutConstraint.constraintsWithVisualFormat("V:|-top-[view]-bottom-|", options: [], metrics: metrics, views: views))
18 | NSLayoutConstraint.activateConstraints(constraints)
19 | return constraints
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/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 | private 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | private 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 | private override func loadView() {
40 | view = _view
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/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 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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/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 | private struct Appearance {
14 | static let LabelTextColor = UIColor(white: 0.56, alpha: 1.0)
15 | static let FieldTextColor = UIColor.blackColor()
16 | static let Font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)
17 | }
18 |
19 | private 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: CGRectZero)
29 | label.textColor = Appearance.LabelTextColor
30 | label.font = Appearance.Font
31 | label.text = labelText
32 | label.setContentHuggingPriority(UILayoutPriorityDefaultHigh, forAxis: .Horizontal)
33 |
34 | textField = UITextField(frame: CGRectZero)
35 | textField.textColor = Appearance.FieldTextColor
36 | textField.font = Appearance.Font
37 |
38 | super.init(frame: CGRectZero)
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/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 | private 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 | private let stackViewContainer: StackViewContainer
21 |
22 | override init(frame: CGRect) {
23 | attachButton = UIButton(type: .Custom)
24 | attachButton.setBackgroundImage(UIImage(named: "attach-button")!, forState: .Normal)
25 | attachButton.adjustsImageWhenHighlighted = true
26 |
27 | let stackView = UIStackView(frame: CGRectZero)
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/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 | public class ImageThumbnailView: UIView {
17 | private 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: CGRectZero)
25 |
26 | let deleteButtonImage = UIImage(named: "delete-button")!
27 | let deleteButton = UIButton(type: .Custom)
28 | deleteButton.translatesAutoresizingMaskIntoConstraints = false
29 | deleteButton.setBackgroundImage(deleteButtonImage, forState: .Normal)
30 | deleteButton.addTarget(self, action: #selector(ImageThumbnailView.didTapDelete(_:)), forControlEvents: .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.constraintsWithVisualFormat("V:|[deleteButton]-imageViewTop-[imageView]|", options: [], metrics: metrics, views: views)
48 | let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|[deleteButton]-imageViewLeft-[imageView]|", options: [], metrics: metrics, views: views)
49 | NSLayoutConstraint.activateConstraints(verticalConstraints)
50 | NSLayoutConstraint.activateConstraints(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 private func didTapDelete(sender: UIButton) {
60 | delegate?.imageThumbnailViewDidTapDeleteButton(self)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/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 | private let stackViewController: StackViewController
14 | private var firstField: UIView?
15 | private var bodyTextView: UITextView?
16 |
17 | init() {
18 | stackViewController = StackViewController()
19 | stackViewController.stackViewContainer.separatorViewFactory = SeparatorView.init
20 |
21 | super.init(nibName: nil, bundle: nil)
22 |
23 | edgesForExtendedLayout = .None
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: CGRectZero)
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 | private 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: CGRectZero)
50 | textView.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)
51 | textView.scrollEnabled = 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 | private func displayStackViewController() {
61 | addChildViewController(stackViewController)
62 | view.addSubview(stackViewController.view)
63 | stackViewController.view.activateSuperviewHuggingConstraints()
64 | stackViewController.didMoveToParentViewController(self)
65 | }
66 |
67 | override func viewDidAppear(animated: Bool) {
68 | super.viewDidAppear(animated)
69 | firstField?.becomeFirstResponder()
70 | }
71 |
72 | // MARK: Actions
73 |
74 | @objc private func send(sender: UIBarButtonItem) {}
75 |
76 | @objc private func didTapView(gestureRecognizer: UIGestureRecognizer) {
77 | bodyTextView?.becomeFirstResponder()
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/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 | public class SeparatorView: UIView {
14 | private 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 | public 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 | private var separatorInset: CGFloat = 15.0 {
28 | didSet { setNeedsDisplay() }
29 | }
30 |
31 | /// The color of the separator
32 | public var separatorColor = UIColor(white: 0.90, alpha: 1.0) {
33 | didSet { setNeedsDisplay() }
34 | }
35 |
36 | /// The axis (horizontal or vertical) of the separator
37 | public var axis = UILayoutConstraintAxis.Horizontal {
38 | didSet { updateSizeConstraint() }
39 | }
40 |
41 | /// Initializes the receiver for display on the specified axis.
42 | public init(axis: UILayoutConstraintAxis) {
43 | self.axis = axis
44 | super.init(frame: CGRectZero)
45 | commonInit()
46 | }
47 |
48 | required public init?(coder aDecoder: NSCoder) {
49 | super.init(coder: aDecoder)
50 | commonInit()
51 | }
52 |
53 | private func updateSizeConstraint() {
54 | sizeConstraint?.active = false
55 | let layoutAttribute: NSLayoutAttribute = {
56 | switch axis {
57 | case .Vertical: return .Height
58 | case .Horizontal: return .Width
59 | }
60 | }()
61 | sizeConstraint = NSLayoutConstraint(
62 | item: self,
63 | attribute: layoutAttribute,
64 | relatedBy: .Equal,
65 | toItem: nil, attribute:
66 | .NotAnAttribute,
67 | multiplier: 1.0,
68 | constant: separatorThickness
69 | )
70 | sizeConstraint?.active = true
71 | }
72 |
73 | private func commonInit() {
74 | backgroundColor = .clearColor()
75 | updateSizeConstraint()
76 | }
77 |
78 | public override func drawRect(rect: CGRect) {
79 | guard separatorThickness > 0 else { return }
80 | let edge: CGRectEdge = {
81 | switch axis {
82 | case .Vertical: return .MinXEdge
83 | case .Horizontal: return .MaxYEdge
84 | }
85 | }()
86 | let (_, separatorRect) = bounds.divide(separatorInset, fromEdge: edge)
87 | separatorColor.setFill()
88 | UIRectFill(separatorRect)
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/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 | private struct Constants {
14 | static let ThumbnailSize = CGSize(width: 96, height: 96)
15 | }
16 |
17 | private 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: CGRectZero)
25 | attachmentView.attachButton.addTarget(self, action: #selector(ImageAttachmentViewController.attachImage(_:)), forControlEvents: .TouchUpInside)
26 | view = attachmentView
27 | self.attachmentView = attachmentView
28 | }
29 |
30 | // MARK: Actions
31 |
32 | @objc private func attachImage(sender: UIButton) {
33 | guard UIImagePickerController.isSourceTypeAvailable(.PhotoLibrary) else { return }
34 | let pickerController = UIImagePickerController()
35 | pickerController.delegate = self
36 | pickerController.sourceType = .PhotoLibrary
37 | presentViewController(pickerController, animated: true, completion: nil)
38 | }
39 |
40 | // MARK: UIImagePickerControllerDelegate
41 |
42 | func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {
43 | dismissViewControllerAnimated(true, completion: nil)
44 | guard let imageURL = info[UIImagePickerControllerReferenceURL] as? NSURL else { return }
45 | getImageThumbnail(imageURL) { image in
46 | if let image = image {
47 | self.attachmentView?.addImageWithThumbnail(image)
48 | }
49 | }
50 | }
51 |
52 | private func getImageThumbnail(imageURL: NSURL, completion: UIImage? -> Void) {
53 | let result = PHAsset.fetchAssetsWithALAssetURLs([imageURL], options: nil)
54 | guard let asset = result.firstObject as? PHAsset else {
55 | completion(nil)
56 | return
57 | }
58 | let imageManager = PHImageManager.defaultManager()
59 | let options = PHImageRequestOptions()
60 | options.resizeMode = .Exact
61 | options.deliveryMode = .HighQualityFormat
62 |
63 | let scale = UIScreen.mainScreen().scale
64 | let targetSize: CGSize = {
65 | var targetSize = Constants.ThumbnailSize
66 | targetSize.width *= scale
67 | targetSize.height *= scale
68 | return targetSize
69 | }()
70 |
71 | imageManager.requestImageForAsset(asset, targetSize: targetSize, contentMode: .AspectFill, options: options) { (image, info) in
72 | let degraded = info?[PHImageResultIsDegradedKey] as? Bool
73 | if degraded == nil || degraded! == false {
74 | if let image = image, CGImage = image.CGImage {
75 | let scaleCorrectedImage = UIImage(CGImage: CGImage, scale: scale, orientation: image.imageOrientation)
76 | completion(scaleCorrectedImage)
77 | } else {
78 | completion(nil)
79 | }
80 | }
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/StackViewController.xcodeproj/xcshareddata/xcschemes/StackViewController.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
64 |
65 |
71 |
72 |
73 |
74 |
75 |
76 |
82 |
83 |
89 |
90 |
91 |
92 |
94 |
95 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/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 | private var stackViewController: StackViewController!
14 |
15 | private class TestViewController: UIViewController {
16 | private 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 | private override func loadView() {
28 | view = UIView(frame: CGRectZero)
29 | view.tag = tag
30 | }
31 | }
32 |
33 | override func setUp() {
34 | super.setUp()
35 | stackViewController = StackViewController()
36 | }
37 |
38 | private func createViewWithTag(tag: Int) -> UIView {
39 | let view = UIView(frame: CGRectZero)
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.childViewControllers.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.childViewControllers.count)
57 | XCTAssertEqual(viewController, stackViewController.childViewControllers[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.childViewControllers.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.childViewControllers.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.childViewControllers.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.childViewControllers.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.childViewControllers.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.childViewControllers.count)
113 | }
114 | }
115 |
116 | private extension StackViewItem {
117 | var tag: Int {
118 | if let view = self as? UIView {
119 | return view.tag
120 | } else if let viewController = self as? UIViewController {
121 | return viewController.view.tag
122 | } else {
123 | return -1
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/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 | public class StackViewController: UIViewController {
17 | /// This is exposed for configuring `backgroundView`, `stackView`
18 | /// `axis`, and `separatorViewFactory`. All other operations should
19 | /// be performed via this controller and not directly via the container view.
20 | public lazy var stackViewContainer = StackViewContainer()
21 |
22 | private var _items = [StackViewItem]()
23 |
24 | /// The items displayed by this controller
25 | public var items: [StackViewItem] {
26 | get { return _items }
27 | set(newItems) {
28 | for (index, _) in _items.enumerate() {
29 | removeItemAtIndex(index)
30 | }
31 | _items = newItems
32 | for item in newItems {
33 | addItem(item, canShowSeparator: true)
34 | }
35 | }
36 | }
37 | private var viewControllers = [UIViewController]()
38 |
39 | public override func loadView() {
40 | view = stackViewContainer
41 | }
42 |
43 | /**
44 | Adds an item to the list of items managed by the controller. The item can
45 | be either a `UIView` or a `UIViewController`, both of which conform to the
46 | `StackViewItem` protocol.
47 |
48 | - parameter item: The item to add
49 | - parameter canShowSeparator: See the documentation for
50 | `StackViewContainer.setCanShowSeparator(:forContentViewAtIndex:)` for more
51 | details on this parameter.
52 | */
53 | public func addItem(item: StackViewItem, canShowSeparator: Bool = true) {
54 | insertItem(item, atIndex: _items.endIndex, canShowSeparator: canShowSeparator)
55 | }
56 |
57 | /**
58 | Inserts an item into the list of items managed by the controller. The item can
59 | be either a `UIView` or a `UIViewController`, both of which conform to the
60 | `StackViewItem` protocol.
61 |
62 | - parameter item: The item to insert
63 | - parameter index: The index to insert the item at
64 | - parameter canShowSeparator: See the documentation for
65 | `StackViewContainer.setCanShowSeparator(:forContentViewAtIndex:)` for more
66 | details on this parameter.
67 | */
68 | public func insertItem(item: StackViewItem, atIndex index: Int, canShowSeparator: Bool = true) {
69 | precondition(index >= _items.startIndex)
70 | precondition(index <= _items.endIndex)
71 |
72 | _items.insert(item, atIndex: index)
73 | let viewController = item.toViewController()
74 | viewControllers.insert(viewController, atIndex: index)
75 | addChildViewController(viewController)
76 | stackViewContainer.insertContentView(viewController.view, atIndex: index, canShowSeparator: canShowSeparator)
77 | }
78 |
79 | /**
80 | Removes an item from the list of items managed by this controller. If `item`
81 | does not exist in `items`, this method does nothing.
82 |
83 | - parameter item: The item to remove.
84 | */
85 | public func removeItem(item: StackViewItem) {
86 | guard let index = _items.indexOf({ $0 === item }) else { return }
87 | removeItemAtIndex(index)
88 | }
89 |
90 | /**
91 | Removes an item from the list of items managed by this controller.
92 |
93 | - parameter index: The index of the item to remove
94 | */
95 | public func removeItemAtIndex(index: Int) {
96 | _items.removeAtIndex(index)
97 | let viewController = viewControllers[index]
98 | viewController.willMoveToParentViewController(nil)
99 | stackViewContainer.removeContentViewAtIndex(index)
100 | viewController.removeFromParentViewController()
101 | viewControllers.removeAtIndex(index)
102 | }
103 |
104 | /**
105 | Sets whether a separator can be shown for `item`
106 |
107 | - parameter canShowSeparator: See the documentation for
108 | `StackViewContainer.setCanShowSeparator(:forContentViewAtIndex:)` for more
109 | details on this parameter.
110 | - parameter item: The item for which to configure separator
111 | visibility
112 | */
113 | public func setCanShowSeparator(canShowSeparator: Bool, forItem item: StackViewItem) {
114 | guard let index = _items.indexOf({ $0 === item }) else { return }
115 | setCanShowSeparator(canShowSeparator, forItemAtIndex: index)
116 | }
117 |
118 | /**
119 | Sets whether a separator can be shown for the item at index `index`
120 |
121 | - parameter canShowSeparator: See the documentation for
122 | `StackViewContainer.setCanShowSeparator(:forContentViewAtIndex:)` for more
123 | details on this parameter.
124 | - parameter index: The index of the item to configure separator
125 | visibility for.
126 | */
127 | public func setCanShowSeparator(canShowSeparator: Bool, forItemAtIndex index: Int) {
128 | stackViewContainer.setCanShowSeparator(canShowSeparator, forContentViewAtIndex: index)
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/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 | public class AutoScrollView: UIScrollView {
15 | private struct Constants {
16 | static let DefaultAnimationDuration: NSTimeInterval = 0.25
17 | static let DefaultAnimationCurve = UIViewAnimationCurve.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 | public var contentView: UIView? {
26 | willSet {
27 | contentView?.removeFromSuperview()
28 | }
29 | didSet {
30 | if let contentView = contentView {
31 | addSubview(contentView)
32 | updateContentViewConstraints()
33 | }
34 | }
35 | }
36 | private var contentViewConstraints: [NSLayoutConstraint]?
37 |
38 | override public var contentInset: UIEdgeInsets {
39 | didSet {
40 | updateContentViewConstraints()
41 | }
42 | }
43 |
44 | private func updateContentViewConstraints() {
45 | if let constraints = contentViewConstraints {
46 | NSLayoutConstraint.deactivateConstraints(constraints)
47 | }
48 | if let contentView = contentView {
49 | contentViewConstraints = contentView.activateSuperviewHuggingConstraints(insets: contentInset)
50 | } else {
51 | contentViewConstraints = nil
52 | }
53 | }
54 |
55 | private func commonInit() {
56 | let nc = NSNotificationCenter.defaultCenter()
57 | nc.addObserver(self, selector: #selector(AutoScrollView.keyboardWillShow(_:)), name: UIKeyboardWillShowNotification, object: nil)
58 | nc.addObserver(self, selector: #selector(AutoScrollView.keyboardWillHide(_:)), name: UIKeyboardWillHideNotification, object: nil)
59 | }
60 |
61 | override init(frame: CGRect) {
62 | super.init(frame: frame)
63 | commonInit()
64 | }
65 |
66 | public required init?(coder aDecoder: NSCoder) {
67 | super.init(coder: aDecoder)
68 | commonInit()
69 | }
70 |
71 | deinit {
72 | NSNotificationCenter.defaultCenter().removeObserver(self)
73 | }
74 |
75 | // MARK: Notifications
76 |
77 | // Implementation based on code from Apple documentation
78 | // https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html
79 | @objc private func keyboardWillShow(notification: NSNotification) {
80 | let keyboardFrameValue = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue
81 | guard var keyboardFrame = keyboardFrameValue?.CGRectValue() else { return }
82 | keyboardFrame = convertRect(keyboardFrame, fromView: nil)
83 |
84 | let bottomInset: CGFloat
85 | let keyboardIntersectionRect = bounds.intersect(keyboardFrame)
86 | if !keyboardIntersectionRect.isNull {
87 | bottomInset = keyboardIntersectionRect.height
88 | let contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0)
89 | super.contentInset = contentInset
90 | scrollIndicatorInsets = contentInset
91 | } else {
92 | bottomInset = 0.0
93 | }
94 |
95 | guard let firstResponder = firstResponder else { return }
96 | let firstResponderFrame = firstResponder.convertRect(firstResponder.bounds, toView: self)
97 |
98 | var contentBounds = CGRect(origin: contentOffset, size: bounds.size)
99 | contentBounds.size.height -= bottomInset
100 | if !contentBounds.contains(firstResponderFrame.origin) {
101 | let duration = notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSTimeInterval ?? Constants.DefaultAnimationDuration
102 | let curve = notification.userInfo?[UIKeyboardAnimationCurveUserInfoKey] as? UIViewAnimationCurve ?? Constants.DefaultAnimationCurve
103 |
104 | // Dropping down to the old style UIView animation API because the new API
105 | // does not support setting the curve directly. The other option is to take
106 | // `curve` and shift it left by 16 bits to turn it into a `UIViewAnimationOptions`,
107 | // but that seems uglier than just doing this.
108 | UIView.beginAnimations(Constants.ScrollAnimationID, context: nil)
109 | UIView.setAnimationCurve(curve)
110 | UIView.setAnimationDuration(duration)
111 | scrollRectToVisible(firstResponderFrame, animated: false)
112 | UIView.commitAnimations()
113 | }
114 | }
115 |
116 | @objc private func keyboardWillHide(notification: NSNotification) {
117 | super.contentInset = UIEdgeInsetsZero
118 | scrollIndicatorInsets = UIEdgeInsetsZero
119 | }
120 | }
121 |
122 | private extension UIView {
123 | var firstResponder: UIView? {
124 | if isFirstResponder() {
125 | return self
126 | }
127 | for subview in subviews {
128 | if let responder = subview.firstResponder {
129 | return responder
130 | }
131 | }
132 | return nil
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # StackViewController
2 | [](https://circleci.com/gh/seedco/StackViewController/tree/master)
3 |
4 | ### Overview
5 |
6 | `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:
7 |
8 |
9 |
10 |
11 |
12 | ### Design Rationale
13 |
14 | 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.
15 |
16 | #### Building Forms with `UITableView` (Is Difficult)
17 |
18 | 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.
19 |
20 | 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.
21 |
22 | The bottom line is that **`UITableView` is the wrong tool for the job**.
23 |
24 | #### Introducing `UIStackView`
25 |
26 | [`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.
27 |
28 | `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.
29 |
30 | #### View Controllers over Views
31 |
32 | 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.
33 |
34 | `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.
35 |
36 | #### View Controller Composition
37 |
38 | [*Composition over inheritance*](https://en.wikipedia.org/wiki/Composition_over_inheritance) is a fundamental principle of object-oriented programming.
39 |
40 | 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.
41 |
42 | 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).
43 |
44 | ### Features
45 |
46 | The framework provides two primary classes: `StackViewContainer` and `StackViewController`. `StackViewContainer` wraps a `UIStackView` and implements the following additional features:
47 |
48 | * **Scrolling support** by embedding the `UIStackView` inside a `UIScrollView` with automatic management of associated constraints
49 | * **Autoscroll behaviour** to automatically adjust layout and scroll to the view being edited when the keyboard appears (the same behaviour implemented by `UITableViewController`)
50 | * **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
51 | * Other minor conveniences like support for background views and changing the background color (since `UIStackView` doesn't draw a background)
52 |
53 | `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.
54 |
55 | ### Example
56 |
57 | 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).
58 |
59 | ### License
60 |
61 | This project is licensed under the MIT license. See `LICENSE.md` for more details.
62 |
--------------------------------------------------------------------------------
/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 | private final class ContentView: UIView {}
14 |
15 | private var stackViewContainer: StackViewContainer!
16 |
17 | override func setUp() {
18 | super.setUp()
19 | stackViewContainer = StackViewContainer()
20 | stackViewContainer.separatorViewFactory = SeparatorView.init
21 | }
22 |
23 | private 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 | stackViewContainer.separatorViewFactory = nil
247 | stackViewContainer.contentViews = [
248 | contentViewWithTag(1),
249 | contentViewWithTag(2)
250 | ]
251 | XCTAssertEqual(2, stackViewContainer.contentViews.count)
252 | XCTAssertEqual([1, 2], stackViewContainer.stackView.arrangedSubviews.map { $0.tag })
253 |
254 | stackViewContainer.separatorViewFactory = SeparatorView.init
255 | XCTAssertEqual(2, stackViewContainer.contentViews.count)
256 | XCTAssertEqual([1, 0, 2], stackViewContainer.stackView.arrangedSubviews.map { $0.tag })
257 | }
258 |
259 | func testAxisSetter() {
260 | stackViewContainer.contentViews = [
261 | contentViewWithTag(1),
262 | contentViewWithTag(2)
263 | ]
264 |
265 | let assertSeparatorAxes: UILayoutConstraintAxis -> Void = { axis in
266 | let separators: [SeparatorView] = self.stackViewContainer.stackView.arrangedSubviews
267 | .filter { $0.isKindOfClass(SeparatorView.self) }
268 | .map { $0 as! SeparatorView }
269 | separators.forEach {
270 | XCTAssertEqual(axis, $0.axis)
271 | }
272 | }
273 |
274 | assertSeparatorAxes(.Vertical)
275 | stackViewContainer.axis = .Horizontal
276 | assertSeparatorAxes(.Horizontal)
277 | }
278 | }
279 |
--------------------------------------------------------------------------------
/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 | public 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 | /// An optional background view that is shown behind the stack view. The
25 | /// top of the background view will be kept pinned to the top of the scroll
26 | /// view bounds, even when bouncing.
27 | public var backgroundView: UIView? {
28 | get { return _backgroundView }
29 | set {
30 | backgroundViewTopConstraint = nil
31 | _backgroundView?.removeFromSuperview()
32 | _backgroundView = newValue
33 | layoutBackgroundView()
34 | }
35 | }
36 | private var _backgroundView: UIView?
37 | private var backgroundViewTopConstraint: NSLayoutConstraint?
38 |
39 | /// The content views that are displayed inside the stack view. This array
40 | /// does not include separator views that are automatically inserted by
41 | /// the container if the `separatorViewFactory` property is set.
42 | ///
43 | /// Setting this array causes all of the existing content views in the
44 | /// stack view to be removed and replaced with the new content views.
45 | public var contentViews: [UIView] {
46 | get { return _contentViews }
47 | set {
48 | _contentViews = newValue
49 | relayoutContent(true)
50 | }
51 | }
52 | private var _contentViews = [UIView]()
53 |
54 | private var items = [Item]()
55 | public var separatorViewFactory: SeparatorViewFactory? {
56 | didSet { relayoutContent(false) }
57 | }
58 |
59 | /// The axis (direction) that content is laid out in. Setting the axis via
60 | /// this property instead of `stackView.axis` ensures that any separator
61 | /// views are recreated to account for the change in layout direction.
62 | public var axis: UILayoutConstraintAxis {
63 | get { return stackView.axis }
64 | set {
65 | stackView.axis = newValue
66 | updateSizeConstraint()
67 | relayoutContent(false)
68 | }
69 | }
70 | private var stackViewSizeConstraint: NSLayoutConstraint?
71 |
72 | public typealias SeparatorViewFactory = UILayoutConstraintAxis -> UIView
73 |
74 | /// Initializes an instance of `StackViewContainer` using a stack view
75 | /// with the default configuration, which is simply a `UIStackView` with
76 | /// all of its properties set to the default values except for `axis`, which
77 | /// is set to `.Vertical`.
78 | public convenience init() {
79 | self.init(stackView: constructDefaultStackView())
80 | }
81 |
82 | /// Initializes an instance of `StackViewContainer` using an existing
83 | /// instance of `UIStackView`. Any existing arranged subviews of the stack
84 | /// view are removed prior to `StackViewContainer` taking ownership of it.
85 | public init(stackView: UIStackView) {
86 | stackView.removeAllArrangedSubviews()
87 | self.stackView = stackView
88 | self.scrollView = AutoScrollView(frame: CGRectZero)
89 | super.init(frame: CGRectZero)
90 | commonInit()
91 | }
92 |
93 | required public init?(coder aDecoder: NSCoder) {
94 | stackView = constructDefaultStackView()
95 | scrollView = AutoScrollView(frame: CGRectZero)
96 | super.init(coder: aDecoder)
97 | commonInit()
98 | }
99 |
100 | private func commonInit() {
101 | scrollView.contentView = stackView
102 | scrollView.delegate = self
103 | addSubview(scrollView)
104 | scrollView.activateSuperviewHuggingConstraints()
105 | updateSizeConstraint()
106 | }
107 |
108 | private func updateSizeConstraint() {
109 | stackViewSizeConstraint?.active = false
110 | let attribute: NSLayoutAttribute = {
111 | switch axis {
112 | case .Horizontal: return .Height
113 | case .Vertical: return .Width
114 | }
115 | }()
116 | stackViewSizeConstraint =
117 | NSLayoutConstraint(item: stackView, attribute: attribute, relatedBy: .Equal, toItem: scrollView, attribute: attribute, multiplier: 1.0, constant: 0.0)
118 | stackViewSizeConstraint?.active = true
119 | }
120 |
121 | private func layoutBackgroundView() {
122 | guard let backgroundView = _backgroundView else { return }
123 | scrollView.insertSubview(backgroundView, atIndex: 0)
124 |
125 | let constraints = backgroundView.activateSuperviewHuggingConstraints()
126 | for constraint in constraints {
127 | if constraint.firstAttribute == .Top {
128 | backgroundViewTopConstraint = constraint
129 | break
130 | }
131 | }
132 | }
133 |
134 | // MARK: Managing Content
135 |
136 | /**
137 | Adds a content view to the list of content views that this container
138 | manages.
139 |
140 | - parameter view: The content view to add
141 | - parameter canShowSeparator: See the documentation for
142 | `StackViewContainer.setCanShowSeparator(:forContentViewAtIndex:)` for more
143 | details on this parameter.
144 | */
145 | public func addContentView(view: UIView, canShowSeparator: Bool = true) {
146 | insertContentView(view, atIndex: items.endIndex, canShowSeparator: canShowSeparator)
147 | }
148 |
149 | /**
150 | Inserts a content view into the list of content views that this container
151 | manages.
152 |
153 | - parameter view: The content view to insert
154 | - parameter index: The index to insert the content view at, in
155 | the `contentViews` array
156 | - parameter canShowSeparator: See the documentation for
157 | `StackViewContainer.setCanShowSeparator(:forContentViewAtIndex:)` for more
158 | details on this parameter.
159 | */
160 | public func insertContentView(view: UIView, atIndex index: Int, canShowSeparator: Bool = true) {
161 | precondition(index >= items.startIndex)
162 | precondition(index <= items.endIndex)
163 |
164 | let stackInsertionIndex: Int
165 | if items.isEmpty {
166 | stackInsertionIndex = 0
167 | } else {
168 | let lastExistingIndex = items.endIndex.predecessor()
169 | let lastItem = items[lastExistingIndex]
170 | if index == lastExistingIndex {
171 | // If a content view is inserted at (items.count - 1), the last
172 | // content item will become the final item in the list, in which
173 | // case its separator should be removed.
174 | if let separatorView = lastItem.separatorView {
175 | stackView.removeArrangedSubview(separatorView)
176 | lastItem.separatorView = nil
177 | }
178 | stackInsertionIndex = indexOfArrangedSubview(lastItem.contentView)
179 | } else if index == items.endIndex {
180 | // If a content view is being inserted at the end of the list, the
181 | // item before it should have a separator added.
182 | if lastItem.separatorView == nil && lastItem.canShowSeparator {
183 | if let separatorView = createSeparatorView() {
184 | lastItem.separatorView = separatorView
185 | stackView.addArrangedSubview(separatorView)
186 | }
187 | }
188 | stackInsertionIndex = stackView.arrangedSubviews.endIndex
189 | } else {
190 | stackInsertionIndex = indexOfArrangedSubview(items[index].contentView)
191 | }
192 | }
193 |
194 | let separatorView: UIView?
195 | // Only show the separator if the item is not the last item in the list
196 | if canShowSeparator && index < items.endIndex {
197 | separatorView = createSeparatorView()
198 | } else {
199 | separatorView = nil
200 | }
201 |
202 | let item = Item(
203 | contentView: view,
204 | canShowSeparator: canShowSeparator,
205 | separatorView: separatorView
206 | )
207 | items.insert(item, atIndex: index)
208 | _contentViews.insert(view, atIndex: index)
209 | stackView.insertArrangedSubview(view, atIndex: stackInsertionIndex)
210 | if let separatorView = separatorView {
211 | stackView.insertArrangedSubview(separatorView, atIndex: stackInsertionIndex.successor())
212 | }
213 | }
214 |
215 | private func indexOfArrangedSubview(subview: UIView) -> Int {
216 | if let index = stackView.arrangedSubviews.indexOf({ $0 === subview }) {
217 | return index
218 | } else {
219 | fatalError("Called indexOfArrangedSubview with subview that doesn't exist in stackView.arrangedSubviews")
220 | }
221 | }
222 |
223 | /**
224 | Removes a content view from the list of content views managed by this container.
225 | If `view` does not exist in `contentViews`, this method does nothing.
226 |
227 | - parameter view: The content view to remove
228 | */
229 | public func removeContentView(view: UIView) {
230 | guard let index = _contentViews.indexOf({ $0 === view }) else { return }
231 | removeContentViewAtIndex(index)
232 | }
233 |
234 | /**
235 | Removes a content view from the list of content views managed by this container.
236 |
237 | - parameter index: The index of the content view to remove
238 | */
239 | public func removeContentViewAtIndex(index: Int) {
240 | precondition(index >= items.startIndex)
241 | precondition(index < items.endIndex)
242 |
243 | let item = items[index]
244 | if items.count >= 1 && index == items.endIndex.predecessor() {
245 | let previousItem = items[index.predecessor()]
246 | if let separatorView = previousItem.separatorView {
247 | stackView.removeArrangedSubview(separatorView)
248 | previousItem.separatorView = nil
249 | }
250 | }
251 | stackView.removeArrangedSubview(item.contentView)
252 | if let separatorView = item.separatorView {
253 | stackView.removeArrangedSubview(separatorView)
254 | }
255 | items.removeAtIndex(index)
256 | _contentViews.removeAtIndex(index)
257 | }
258 |
259 | /**
260 | Controls the visibility of the separator view that comes after a content view.
261 | If `view` does not exist in `contentViews`, this method does nothing.
262 |
263 | - parameter canShowSeparator: See the documentation for
264 | `StackViewContainer.setCanShowSeparator(:forContentViewAtIndex:)` for more
265 | details on this parameter.
266 | - parameter view: The content view for which to set separator
267 | visibility.
268 | */
269 | public func setCanShowSeparator(canShowSeparator: Bool, forContentView view: UIView) {
270 | guard let index = _contentViews.indexOf({ $0 === view }) else { return }
271 | setCanShowSeparator(canShowSeparator, forContentViewAtIndex: index)
272 | }
273 |
274 | /**
275 | Controls the visibility of the separator view that comes after a content view.
276 |
277 | - parameter canShowSeparator: Whether it is possible for the content view
278 | to show a separator view *after* it (i.e. to the right of the content view
279 | if the stack view orientation is horizontal, and to the bottom of the
280 | content view if the stack view orientation is vertical). A separator will
281 | not be shown if the content view is the last content view in the list.
282 | - parameter index: The index of the content view for which to
283 | set separator visibility.
284 | */
285 | public func setCanShowSeparator(canShowSeparator: Bool, forContentViewAtIndex index: Int) {
286 | let item = items[index]
287 | if canShowSeparator
288 | && (index < items.endIndex.predecessor())
289 | && item.separatorView == nil {
290 | if let separatorView = createSeparatorView() {
291 | item.separatorView = separatorView
292 | stackView.insertArrangedSubview(separatorView, atIndex: index.successor())
293 | }
294 | } else if let separatorView = item.separatorView where !canShowSeparator {
295 | stackView.removeArrangedSubview(separatorView)
296 | item.separatorView = nil
297 | }
298 | }
299 |
300 | private func relayoutContent(didUpdateContent: Bool) {
301 | let canShowSeparatorConfig: [Bool]?
302 | if didUpdateContent {
303 | canShowSeparatorConfig = nil
304 | } else {
305 | canShowSeparatorConfig = items.map { $0.canShowSeparator }
306 | }
307 | let canShowSeparator: (Int -> Bool) = { index in
308 | if let canShowSeparatorConfig = canShowSeparatorConfig {
309 | return canShowSeparatorConfig[index]
310 | } else {
311 | return true
312 | }
313 | }
314 | items.removeAll(keepCapacity: true)
315 | stackView.removeAllArrangedSubviews()
316 | let contentViews = _contentViews
317 | _contentViews.removeAll(keepCapacity: true)
318 | for (index, contentView) in contentViews.enumerate() {
319 | addContentView(contentView, canShowSeparator: canShowSeparator(index))
320 | }
321 | }
322 |
323 | private func createSeparatorView() -> UIView? {
324 | guard let separatorViewFactory = separatorViewFactory else { return nil }
325 | return separatorViewFactory(stackView.axis)
326 | }
327 |
328 | // MARK: UIScrollViewDelegate
329 |
330 | public func scrollViewDidScroll(scrollView: UIScrollView) {
331 | guard let backgroundViewTopConstraint = backgroundViewTopConstraint else { return }
332 | backgroundViewTopConstraint.constant = -max(-scrollView.contentOffset.y, 0)
333 | }
334 |
335 | // MARK: ContentContainerView
336 |
337 | private class Item {
338 | private let contentView: UIView
339 | private let canShowSeparator: Bool
340 | private var separatorView: UIView?
341 |
342 | init(contentView: UIView, canShowSeparator: Bool, separatorView: UIView?) {
343 | self.contentView = contentView
344 | self.canShowSeparator = canShowSeparator
345 | self.separatorView = separatorView
346 | }
347 | }
348 | }
349 |
350 | private func constructDefaultStackView() -> UIStackView {
351 | let stackView = UIStackView(frame: CGRectZero)
352 | stackView.axis = .Vertical
353 | return stackView
354 | }
355 |
--------------------------------------------------------------------------------
/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 | /* End PBXBuildFile section */
33 |
34 | /* Begin PBXContainerItemProxy section */
35 | 72D193EF1CC3576A00645F83 /* PBXContainerItemProxy */ = {
36 | isa = PBXContainerItemProxy;
37 | containerPortal = 72B21E4D1CBC34EF00724EB8 /* Project object */;
38 | proxyType = 1;
39 | remoteGlobalIDString = 72D193E31CC3576A00645F83;
40 | remoteInfo = StackViewController;
41 | };
42 | 72D1941F1CC357A800645F83 /* PBXContainerItemProxy */ = {
43 | isa = PBXContainerItemProxy;
44 | containerPortal = 72B21E4D1CBC34EF00724EB8 /* Project object */;
45 | proxyType = 1;
46 | remoteGlobalIDString = 72D193E31CC3576A00645F83;
47 | remoteInfo = StackViewController;
48 | };
49 | /* End PBXContainerItemProxy section */
50 |
51 | /* Begin PBXCopyFilesBuildPhase section */
52 | 72D194211CC357A800645F83 /* Embed Frameworks */ = {
53 | isa = PBXCopyFilesBuildPhase;
54 | buildActionMask = 2147483647;
55 | dstPath = "";
56 | dstSubfolderSpec = 10;
57 | files = (
58 | 72D1941E1CC357A800645F83 /* StackViewController.framework in Embed Frameworks */,
59 | );
60 | name = "Embed Frameworks";
61 | runOnlyForDeploymentPostprocessing = 0;
62 | };
63 | /* End PBXCopyFilesBuildPhase section */
64 |
65 | /* Begin PBXFileReference section */
66 | 72D193E41CC3576A00645F83 /* StackViewController.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StackViewController.framework; sourceTree = BUILT_PRODUCTS_DIR; };
67 | 72D193E61CC3576A00645F83 /* StackViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StackViewController.h; sourceTree = ""; };
68 | 72D193E81CC3576A00645F83 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
69 | 72D193ED1CC3576A00645F83 /* StackViewControllerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StackViewControllerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
70 | 72D193F21CC3576A00645F83 /* StackViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackViewControllerTests.swift; sourceTree = ""; };
71 | 72D193F41CC3576A00645F83 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
72 | 72D193FB1CC3577B00645F83 /* AutoScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoScrollView.swift; sourceTree = ""; };
73 | 72D193FD1CC3577B00645F83 /* SeparatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeparatorView.swift; sourceTree = ""; };
74 | 72D193FE1CC3577B00645F83 /* StackViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackViewContainer.swift; sourceTree = ""; };
75 | 72D193FF1CC3577B00645F83 /* StackViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackViewController.swift; sourceTree = ""; };
76 | 72D194001CC3577B00645F83 /* StackViewItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackViewItem.swift; sourceTree = ""; };
77 | 72D1940B1CC357A100645F83 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
78 | 72D1940D1CC357A100645F83 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
79 | 72D1940F1CC357A100645F83 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
80 | 72D194141CC357A100645F83 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
81 | 72D194171CC357A100645F83 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
82 | 72D194191CC357A100645F83 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
83 | 72D194221CC35CDC00645F83 /* StackViewContainerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackViewContainerTests.swift; sourceTree = ""; };
84 | 72DE371E1CCDA961009CAE32 /* LabeledTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabeledTextField.swift; sourceTree = ""; };
85 | 72DE37201CCDABA8009CAE32 /* UIViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = ""; };
86 | 72DE37221CCDB9AE009CAE32 /* LabeledTextFieldController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabeledTextFieldController.swift; sourceTree = ""; };
87 | 72DE37241CCDC75D009CAE32 /* ImageAttachmentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageAttachmentView.swift; sourceTree = ""; };
88 | 72DE37261CCDC769009CAE32 /* ImageAttachmentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageAttachmentViewController.swift; sourceTree = ""; };
89 | 72DE37281CCDCCB6009CAE32 /* UIStackViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIStackViewExtensions.swift; sourceTree = ""; };
90 | 72DE372A1CCDD998009CAE32 /* ImageThumbnailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageThumbnailView.swift; sourceTree = ""; };
91 | /* End PBXFileReference section */
92 |
93 | /* Begin PBXFrameworksBuildPhase section */
94 | 72D193E01CC3576A00645F83 /* Frameworks */ = {
95 | isa = PBXFrameworksBuildPhase;
96 | buildActionMask = 2147483647;
97 | files = (
98 | );
99 | runOnlyForDeploymentPostprocessing = 0;
100 | };
101 | 72D193EA1CC3576A00645F83 /* Frameworks */ = {
102 | isa = PBXFrameworksBuildPhase;
103 | buildActionMask = 2147483647;
104 | files = (
105 | 72D193EE1CC3576A00645F83 /* StackViewController.framework in Frameworks */,
106 | );
107 | runOnlyForDeploymentPostprocessing = 0;
108 | };
109 | 72D194081CC357A100645F83 /* Frameworks */ = {
110 | isa = PBXFrameworksBuildPhase;
111 | buildActionMask = 2147483647;
112 | files = (
113 | 72D1941D1CC357A800645F83 /* StackViewController.framework in Frameworks */,
114 | );
115 | runOnlyForDeploymentPostprocessing = 0;
116 | };
117 | /* End PBXFrameworksBuildPhase section */
118 |
119 | /* Begin PBXGroup section */
120 | 72B21E4C1CBC34EF00724EB8 = {
121 | isa = PBXGroup;
122 | children = (
123 | 72D193E51CC3576A00645F83 /* StackViewController */,
124 | 72D193F11CC3576A00645F83 /* StackViewControllerTests */,
125 | 72D1940C1CC357A100645F83 /* Example */,
126 | 72B21E561CBC34EF00724EB8 /* Products */,
127 | );
128 | sourceTree = "";
129 | };
130 | 72B21E561CBC34EF00724EB8 /* Products */ = {
131 | isa = PBXGroup;
132 | children = (
133 | 72D193E41CC3576A00645F83 /* StackViewController.framework */,
134 | 72D193ED1CC3576A00645F83 /* StackViewControllerTests.xctest */,
135 | 72D1940B1CC357A100645F83 /* Example.app */,
136 | );
137 | name = Products;
138 | sourceTree = "";
139 | };
140 | 72D193E51CC3576A00645F83 /* StackViewController */ = {
141 | isa = PBXGroup;
142 | children = (
143 | 72D193E61CC3576A00645F83 /* StackViewController.h */,
144 | 72D193FB1CC3577B00645F83 /* AutoScrollView.swift */,
145 | 72D193FD1CC3577B00645F83 /* SeparatorView.swift */,
146 | 72D193FE1CC3577B00645F83 /* StackViewContainer.swift */,
147 | 72D193FF1CC3577B00645F83 /* StackViewController.swift */,
148 | 72D194001CC3577B00645F83 /* StackViewItem.swift */,
149 | 72D193E81CC3576A00645F83 /* Info.plist */,
150 | 72DE37201CCDABA8009CAE32 /* UIViewExtensions.swift */,
151 | 72DE37281CCDCCB6009CAE32 /* UIStackViewExtensions.swift */,
152 | );
153 | path = StackViewController;
154 | sourceTree = "";
155 | };
156 | 72D193F11CC3576A00645F83 /* StackViewControllerTests */ = {
157 | isa = PBXGroup;
158 | children = (
159 | 72D193F21CC3576A00645F83 /* StackViewControllerTests.swift */,
160 | 72D194221CC35CDC00645F83 /* StackViewContainerTests.swift */,
161 | 72D193F41CC3576A00645F83 /* Info.plist */,
162 | );
163 | path = StackViewControllerTests;
164 | sourceTree = "";
165 | };
166 | 72D1940C1CC357A100645F83 /* Example */ = {
167 | isa = PBXGroup;
168 | children = (
169 | 72D1940D1CC357A100645F83 /* AppDelegate.swift */,
170 | 72D1940F1CC357A100645F83 /* ViewController.swift */,
171 | 72D194141CC357A100645F83 /* Assets.xcassets */,
172 | 72D194161CC357A100645F83 /* LaunchScreen.storyboard */,
173 | 72D194191CC357A100645F83 /* Info.plist */,
174 | 72DE371E1CCDA961009CAE32 /* LabeledTextField.swift */,
175 | 72DE37221CCDB9AE009CAE32 /* LabeledTextFieldController.swift */,
176 | 72DE37241CCDC75D009CAE32 /* ImageAttachmentView.swift */,
177 | 72DE37261CCDC769009CAE32 /* ImageAttachmentViewController.swift */,
178 | 72DE372A1CCDD998009CAE32 /* ImageThumbnailView.swift */,
179 | );
180 | path = Example;
181 | sourceTree = "";
182 | };
183 | /* End PBXGroup section */
184 |
185 | /* Begin PBXHeadersBuildPhase section */
186 | 72D193E11CC3576A00645F83 /* Headers */ = {
187 | isa = PBXHeadersBuildPhase;
188 | buildActionMask = 2147483647;
189 | files = (
190 | 72D193E71CC3576A00645F83 /* StackViewController.h in Headers */,
191 | );
192 | runOnlyForDeploymentPostprocessing = 0;
193 | };
194 | /* End PBXHeadersBuildPhase section */
195 |
196 | /* Begin PBXNativeTarget section */
197 | 72D193E31CC3576A00645F83 /* StackViewController */ = {
198 | isa = PBXNativeTarget;
199 | buildConfigurationList = 72D193F51CC3576A00645F83 /* Build configuration list for PBXNativeTarget "StackViewController" */;
200 | buildPhases = (
201 | 72D193DF1CC3576A00645F83 /* Sources */,
202 | 72D193E01CC3576A00645F83 /* Frameworks */,
203 | 72D193E11CC3576A00645F83 /* Headers */,
204 | 72D193E21CC3576A00645F83 /* Resources */,
205 | );
206 | buildRules = (
207 | );
208 | dependencies = (
209 | );
210 | name = StackViewController;
211 | productName = StackViewController;
212 | productReference = 72D193E41CC3576A00645F83 /* StackViewController.framework */;
213 | productType = "com.apple.product-type.framework";
214 | };
215 | 72D193EC1CC3576A00645F83 /* StackViewControllerTests */ = {
216 | isa = PBXNativeTarget;
217 | buildConfigurationList = 72D193F81CC3576A00645F83 /* Build configuration list for PBXNativeTarget "StackViewControllerTests" */;
218 | buildPhases = (
219 | 72D193E91CC3576A00645F83 /* Sources */,
220 | 72D193EA1CC3576A00645F83 /* Frameworks */,
221 | 72D193EB1CC3576A00645F83 /* Resources */,
222 | );
223 | buildRules = (
224 | );
225 | dependencies = (
226 | 72D193F01CC3576A00645F83 /* PBXTargetDependency */,
227 | );
228 | name = StackViewControllerTests;
229 | productName = StackViewControllerTests;
230 | productReference = 72D193ED1CC3576A00645F83 /* StackViewControllerTests.xctest */;
231 | productType = "com.apple.product-type.bundle.unit-test";
232 | };
233 | 72D1940A1CC357A100645F83 /* Example */ = {
234 | isa = PBXNativeTarget;
235 | buildConfigurationList = 72D1941A1CC357A100645F83 /* Build configuration list for PBXNativeTarget "Example" */;
236 | buildPhases = (
237 | 72D194071CC357A100645F83 /* Sources */,
238 | 72D194081CC357A100645F83 /* Frameworks */,
239 | 72D194091CC357A100645F83 /* Resources */,
240 | 72D194211CC357A800645F83 /* Embed Frameworks */,
241 | );
242 | buildRules = (
243 | );
244 | dependencies = (
245 | 72D194201CC357A800645F83 /* PBXTargetDependency */,
246 | );
247 | name = Example;
248 | productName = Example;
249 | productReference = 72D1940B1CC357A100645F83 /* Example.app */;
250 | productType = "com.apple.product-type.application";
251 | };
252 | /* End PBXNativeTarget section */
253 |
254 | /* Begin PBXProject section */
255 | 72B21E4D1CBC34EF00724EB8 /* Project object */ = {
256 | isa = PBXProject;
257 | attributes = {
258 | LastSwiftUpdateCheck = 0730;
259 | LastUpgradeCheck = 0730;
260 | ORGANIZATIONNAME = "Seed Platform, Inc";
261 | TargetAttributes = {
262 | 72D193E31CC3576A00645F83 = {
263 | CreatedOnToolsVersion = 7.3;
264 | };
265 | 72D193EC1CC3576A00645F83 = {
266 | CreatedOnToolsVersion = 7.3;
267 | };
268 | 72D1940A1CC357A100645F83 = {
269 | CreatedOnToolsVersion = 7.3;
270 | };
271 | };
272 | };
273 | buildConfigurationList = 72B21E501CBC34EF00724EB8 /* Build configuration list for PBXProject "StackViewController" */;
274 | compatibilityVersion = "Xcode 3.2";
275 | developmentRegion = English;
276 | hasScannedForEncodings = 0;
277 | knownRegions = (
278 | en,
279 | Base,
280 | );
281 | mainGroup = 72B21E4C1CBC34EF00724EB8;
282 | productRefGroup = 72B21E561CBC34EF00724EB8 /* Products */;
283 | projectDirPath = "";
284 | projectRoot = "";
285 | targets = (
286 | 72D193E31CC3576A00645F83 /* StackViewController */,
287 | 72D193EC1CC3576A00645F83 /* StackViewControllerTests */,
288 | 72D1940A1CC357A100645F83 /* Example */,
289 | );
290 | };
291 | /* End PBXProject section */
292 |
293 | /* Begin PBXResourcesBuildPhase section */
294 | 72D193E21CC3576A00645F83 /* Resources */ = {
295 | isa = PBXResourcesBuildPhase;
296 | buildActionMask = 2147483647;
297 | files = (
298 | );
299 | runOnlyForDeploymentPostprocessing = 0;
300 | };
301 | 72D193EB1CC3576A00645F83 /* Resources */ = {
302 | isa = PBXResourcesBuildPhase;
303 | buildActionMask = 2147483647;
304 | files = (
305 | );
306 | runOnlyForDeploymentPostprocessing = 0;
307 | };
308 | 72D194091CC357A100645F83 /* Resources */ = {
309 | isa = PBXResourcesBuildPhase;
310 | buildActionMask = 2147483647;
311 | files = (
312 | 72D194181CC357A100645F83 /* LaunchScreen.storyboard in Resources */,
313 | 72D194151CC357A100645F83 /* Assets.xcassets in Resources */,
314 | );
315 | runOnlyForDeploymentPostprocessing = 0;
316 | };
317 | /* End PBXResourcesBuildPhase section */
318 |
319 | /* Begin PBXSourcesBuildPhase section */
320 | 72D193DF1CC3576A00645F83 /* Sources */ = {
321 | isa = PBXSourcesBuildPhase;
322 | buildActionMask = 2147483647;
323 | files = (
324 | 72DE37211CCDABA8009CAE32 /* UIViewExtensions.swift in Sources */,
325 | 72D194031CC3577B00645F83 /* SeparatorView.swift in Sources */,
326 | 72DE37291CCDCCB6009CAE32 /* UIStackViewExtensions.swift in Sources */,
327 | 72D194061CC3577B00645F83 /* StackViewItem.swift in Sources */,
328 | 72D194051CC3577B00645F83 /* StackViewController.swift in Sources */,
329 | 72D194011CC3577B00645F83 /* AutoScrollView.swift in Sources */,
330 | 72D194041CC3577B00645F83 /* StackViewContainer.swift in Sources */,
331 | );
332 | runOnlyForDeploymentPostprocessing = 0;
333 | };
334 | 72D193E91CC3576A00645F83 /* Sources */ = {
335 | isa = PBXSourcesBuildPhase;
336 | buildActionMask = 2147483647;
337 | files = (
338 | 72D193F31CC3576A00645F83 /* StackViewControllerTests.swift in Sources */,
339 | 72D194231CC35CDC00645F83 /* StackViewContainerTests.swift in Sources */,
340 | );
341 | runOnlyForDeploymentPostprocessing = 0;
342 | };
343 | 72D194071CC357A100645F83 /* Sources */ = {
344 | isa = PBXSourcesBuildPhase;
345 | buildActionMask = 2147483647;
346 | files = (
347 | 72D194101CC357A100645F83 /* ViewController.swift in Sources */,
348 | 72D1940E1CC357A100645F83 /* AppDelegate.swift in Sources */,
349 | 72DE372B1CCDD998009CAE32 /* ImageThumbnailView.swift in Sources */,
350 | 72DE37231CCDB9AE009CAE32 /* LabeledTextFieldController.swift in Sources */,
351 | 72DE37271CCDC769009CAE32 /* ImageAttachmentViewController.swift in Sources */,
352 | 72DE37251CCDC75D009CAE32 /* ImageAttachmentView.swift in Sources */,
353 | 72DE371F1CCDA961009CAE32 /* LabeledTextField.swift in Sources */,
354 | );
355 | runOnlyForDeploymentPostprocessing = 0;
356 | };
357 | /* End PBXSourcesBuildPhase section */
358 |
359 | /* Begin PBXTargetDependency section */
360 | 72D193F01CC3576A00645F83 /* PBXTargetDependency */ = {
361 | isa = PBXTargetDependency;
362 | target = 72D193E31CC3576A00645F83 /* StackViewController */;
363 | targetProxy = 72D193EF1CC3576A00645F83 /* PBXContainerItemProxy */;
364 | };
365 | 72D194201CC357A800645F83 /* PBXTargetDependency */ = {
366 | isa = PBXTargetDependency;
367 | target = 72D193E31CC3576A00645F83 /* StackViewController */;
368 | targetProxy = 72D1941F1CC357A800645F83 /* PBXContainerItemProxy */;
369 | };
370 | /* End PBXTargetDependency section */
371 |
372 | /* Begin PBXVariantGroup section */
373 | 72D194161CC357A100645F83 /* LaunchScreen.storyboard */ = {
374 | isa = PBXVariantGroup;
375 | children = (
376 | 72D194171CC357A100645F83 /* Base */,
377 | );
378 | name = LaunchScreen.storyboard;
379 | sourceTree = "";
380 | };
381 | /* End PBXVariantGroup section */
382 |
383 | /* Begin XCBuildConfiguration section */
384 | 72B21E651CBC34F000724EB8 /* Debug */ = {
385 | isa = XCBuildConfiguration;
386 | buildSettings = {
387 | ALWAYS_SEARCH_USER_PATHS = NO;
388 | CLANG_ANALYZER_NONNULL = YES;
389 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
390 | CLANG_CXX_LIBRARY = "libc++";
391 | CLANG_ENABLE_MODULES = YES;
392 | CLANG_ENABLE_OBJC_ARC = YES;
393 | CLANG_WARN_BOOL_CONVERSION = YES;
394 | CLANG_WARN_CONSTANT_CONVERSION = YES;
395 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
396 | CLANG_WARN_EMPTY_BODY = YES;
397 | CLANG_WARN_ENUM_CONVERSION = YES;
398 | CLANG_WARN_INT_CONVERSION = YES;
399 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
400 | CLANG_WARN_UNREACHABLE_CODE = YES;
401 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
402 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
403 | COPY_PHASE_STRIP = NO;
404 | DEBUG_INFORMATION_FORMAT = dwarf;
405 | ENABLE_STRICT_OBJC_MSGSEND = YES;
406 | ENABLE_TESTABILITY = YES;
407 | GCC_C_LANGUAGE_STANDARD = gnu99;
408 | GCC_DYNAMIC_NO_PIC = NO;
409 | GCC_NO_COMMON_BLOCKS = YES;
410 | GCC_OPTIMIZATION_LEVEL = 0;
411 | GCC_PREPROCESSOR_DEFINITIONS = (
412 | "DEBUG=1",
413 | "$(inherited)",
414 | );
415 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
416 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
417 | GCC_WARN_UNDECLARED_SELECTOR = YES;
418 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
419 | GCC_WARN_UNUSED_FUNCTION = YES;
420 | GCC_WARN_UNUSED_VARIABLE = YES;
421 | IPHONEOS_DEPLOYMENT_TARGET = 9.3;
422 | MTL_ENABLE_DEBUG_INFO = YES;
423 | ONLY_ACTIVE_ARCH = YES;
424 | SDKROOT = iphoneos;
425 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
426 | };
427 | name = Debug;
428 | };
429 | 72B21E661CBC34F000724EB8 /* Release */ = {
430 | isa = XCBuildConfiguration;
431 | buildSettings = {
432 | ALWAYS_SEARCH_USER_PATHS = NO;
433 | CLANG_ANALYZER_NONNULL = YES;
434 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
435 | CLANG_CXX_LIBRARY = "libc++";
436 | CLANG_ENABLE_MODULES = YES;
437 | CLANG_ENABLE_OBJC_ARC = YES;
438 | CLANG_WARN_BOOL_CONVERSION = YES;
439 | CLANG_WARN_CONSTANT_CONVERSION = YES;
440 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
441 | CLANG_WARN_EMPTY_BODY = YES;
442 | CLANG_WARN_ENUM_CONVERSION = YES;
443 | CLANG_WARN_INT_CONVERSION = YES;
444 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
445 | CLANG_WARN_UNREACHABLE_CODE = YES;
446 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
447 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
448 | COPY_PHASE_STRIP = NO;
449 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
450 | ENABLE_NS_ASSERTIONS = NO;
451 | ENABLE_STRICT_OBJC_MSGSEND = YES;
452 | GCC_C_LANGUAGE_STANDARD = gnu99;
453 | GCC_NO_COMMON_BLOCKS = YES;
454 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
455 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
456 | GCC_WARN_UNDECLARED_SELECTOR = YES;
457 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
458 | GCC_WARN_UNUSED_FUNCTION = YES;
459 | GCC_WARN_UNUSED_VARIABLE = YES;
460 | IPHONEOS_DEPLOYMENT_TARGET = 9.3;
461 | MTL_ENABLE_DEBUG_INFO = NO;
462 | SDKROOT = iphoneos;
463 | VALIDATE_PRODUCT = YES;
464 | };
465 | name = Release;
466 | };
467 | 72D193F61CC3576A00645F83 /* Debug */ = {
468 | isa = XCBuildConfiguration;
469 | buildSettings = {
470 | CLANG_ENABLE_MODULES = YES;
471 | CURRENT_PROJECT_VERSION = 1;
472 | DEFINES_MODULE = YES;
473 | DYLIB_COMPATIBILITY_VERSION = 1;
474 | DYLIB_CURRENT_VERSION = 1;
475 | DYLIB_INSTALL_NAME_BASE = "@rpath";
476 | INFOPLIST_FILE = StackViewController/Info.plist;
477 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
478 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
479 | PRODUCT_BUNDLE_IDENTIFIER = co.seed.StackViewController;
480 | PRODUCT_NAME = "$(TARGET_NAME)";
481 | SKIP_INSTALL = YES;
482 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
483 | TARGETED_DEVICE_FAMILY = "1,2";
484 | VERSIONING_SYSTEM = "apple-generic";
485 | VERSION_INFO_PREFIX = "";
486 | };
487 | name = Debug;
488 | };
489 | 72D193F71CC3576A00645F83 /* Release */ = {
490 | isa = XCBuildConfiguration;
491 | buildSettings = {
492 | CLANG_ENABLE_MODULES = YES;
493 | CURRENT_PROJECT_VERSION = 1;
494 | DEFINES_MODULE = YES;
495 | DYLIB_COMPATIBILITY_VERSION = 1;
496 | DYLIB_CURRENT_VERSION = 1;
497 | DYLIB_INSTALL_NAME_BASE = "@rpath";
498 | INFOPLIST_FILE = StackViewController/Info.plist;
499 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
500 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
501 | PRODUCT_BUNDLE_IDENTIFIER = co.seed.StackViewController;
502 | PRODUCT_NAME = "$(TARGET_NAME)";
503 | SKIP_INSTALL = YES;
504 | TARGETED_DEVICE_FAMILY = "1,2";
505 | VERSIONING_SYSTEM = "apple-generic";
506 | VERSION_INFO_PREFIX = "";
507 | };
508 | name = Release;
509 | };
510 | 72D193F91CC3576A00645F83 /* Debug */ = {
511 | isa = XCBuildConfiguration;
512 | buildSettings = {
513 | INFOPLIST_FILE = StackViewControllerTests/Info.plist;
514 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
515 | PRODUCT_BUNDLE_IDENTIFIER = co.seed.StackViewControllerTests;
516 | PRODUCT_NAME = "$(TARGET_NAME)";
517 | };
518 | name = Debug;
519 | };
520 | 72D193FA1CC3576A00645F83 /* Release */ = {
521 | isa = XCBuildConfiguration;
522 | buildSettings = {
523 | INFOPLIST_FILE = StackViewControllerTests/Info.plist;
524 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
525 | PRODUCT_BUNDLE_IDENTIFIER = co.seed.StackViewControllerTests;
526 | PRODUCT_NAME = "$(TARGET_NAME)";
527 | };
528 | name = Release;
529 | };
530 | 72D1941B1CC357A100645F83 /* Debug */ = {
531 | isa = XCBuildConfiguration;
532 | buildSettings = {
533 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
534 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES;
535 | INFOPLIST_FILE = Example/Info.plist;
536 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
537 | PRODUCT_BUNDLE_IDENTIFIER = co.seed.Example;
538 | PRODUCT_NAME = "$(TARGET_NAME)";
539 | };
540 | name = Debug;
541 | };
542 | 72D1941C1CC357A100645F83 /* Release */ = {
543 | isa = XCBuildConfiguration;
544 | buildSettings = {
545 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
546 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES;
547 | INFOPLIST_FILE = Example/Info.plist;
548 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
549 | PRODUCT_BUNDLE_IDENTIFIER = co.seed.Example;
550 | PRODUCT_NAME = "$(TARGET_NAME)";
551 | };
552 | name = Release;
553 | };
554 | /* End XCBuildConfiguration section */
555 |
556 | /* Begin XCConfigurationList section */
557 | 72B21E501CBC34EF00724EB8 /* Build configuration list for PBXProject "StackViewController" */ = {
558 | isa = XCConfigurationList;
559 | buildConfigurations = (
560 | 72B21E651CBC34F000724EB8 /* Debug */,
561 | 72B21E661CBC34F000724EB8 /* Release */,
562 | );
563 | defaultConfigurationIsVisible = 0;
564 | defaultConfigurationName = Release;
565 | };
566 | 72D193F51CC3576A00645F83 /* Build configuration list for PBXNativeTarget "StackViewController" */ = {
567 | isa = XCConfigurationList;
568 | buildConfigurations = (
569 | 72D193F61CC3576A00645F83 /* Debug */,
570 | 72D193F71CC3576A00645F83 /* Release */,
571 | );
572 | defaultConfigurationIsVisible = 0;
573 | defaultConfigurationName = Release;
574 | };
575 | 72D193F81CC3576A00645F83 /* Build configuration list for PBXNativeTarget "StackViewControllerTests" */ = {
576 | isa = XCConfigurationList;
577 | buildConfigurations = (
578 | 72D193F91CC3576A00645F83 /* Debug */,
579 | 72D193FA1CC3576A00645F83 /* Release */,
580 | );
581 | defaultConfigurationIsVisible = 0;
582 | defaultConfigurationName = Release;
583 | };
584 | 72D1941A1CC357A100645F83 /* Build configuration list for PBXNativeTarget "Example" */ = {
585 | isa = XCConfigurationList;
586 | buildConfigurations = (
587 | 72D1941B1CC357A100645F83 /* Debug */,
588 | 72D1941C1CC357A100645F83 /* Release */,
589 | );
590 | defaultConfigurationIsVisible = 0;
591 | defaultConfigurationName = Release;
592 | };
593 | /* End XCConfigurationList section */
594 | };
595 | rootObject = 72B21E4D1CBC34EF00724EB8 /* Project object */;
596 | }
597 |
--------------------------------------------------------------------------------