├── 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 | [![Circle CI](https://circleci.com/gh/seedco/StackViewController/tree/master.svg?style=svg)](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 | StackViewController Example App 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 | --------------------------------------------------------------------------------