├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.swift ├── README.md ├── Sources └── EquallySpacedStack │ └── EquallySpacedStack.swift ├── Tests ├── EquallySpacedStackTests │ ├── EquallySpacedStackTests.swift │ └── XCTestManifests.swift └── LinuxMain.swift └── distributed_stack-aaae1c88c4412c44c53045f94508be1c-103f7.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | .swiftpm/xcode/xcuserdata 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "EquallySpacedStack", 8 | platforms: [ 9 | .iOS(.v13) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 13 | .library( 14 | name: "EquallySpacedStack", 15 | targets: ["EquallySpacedStack"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 24 | .target( 25 | name: "EquallySpacedStack", 26 | dependencies: []), 27 | .testTarget( 28 | name: "EquallySpacedStackTests", 29 | dependencies: ["EquallySpacedStack"]), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EquallySpacedStack 2 | 3 | This package provides two new Views: `EquallySpacedHStack` and `EquallySpacedVStack`. They behave like their respective Stacks but distribute the subviews by adding equal space between them. 4 | 5 | ## Status 6 | 7 | This package is work in progress. I'm still trying to figure out the best way of doing custom layouts in SwiftUI. 8 | 9 | If you want to follow the process you can read my blog or [follow me](https://twitter.com/alexito4) on Twitter: 10 | - [Trying to understand custom layouts in SwiftUI](https://alejandromp.com/blog/2019/06/12/custom-layouts-in-swiftui/) 11 | - [SwiftUI TupleView and equally spaced stacks](https://alejandromp.com/blog/2019/06/13/implementing-a-equally-spaced-stack-in-swiftui-thanks-to-tupleview/) 12 | 13 | ## Usage 14 | 15 | Just replace `HStack` or `VStack` with `EquallySpacedHStack` or `EquallySpacedVStack`: 16 | 17 | ``` 18 | EquallySpacedHStack { 19 | Color.red.frame(width: 50, height: 50) 20 | Color.blue.frame(width: 50, height: 50) 21 | Color.green.frame(width: 50, height: 50) 22 | } 23 | ``` 24 | you can also use `ForEach`: 25 | 26 | ``` 27 | EquallySpacedHStack { 28 | ForEach(1...4) { _ in 29 | Color.red.frame(width: 50, height: 50) 30 | } 31 | } 32 | ``` 33 | 34 | ![](distributed_stack-aaae1c88c4412c44c53045f94508be1c-103f7.png) 35 | 36 | ## WIP 37 | 38 | - [ ] Avoid issue when using more than the supported number of nested subviews. 39 | - [ ] Support more nested subviews (max is now 4) 40 | - [ ] Avoid using the type erased `AnyView` 41 | 42 | ## Contributions & support 43 | 44 | This project is developed completely in the open, and your contributions are more than welcome. 45 | 46 | This project does not come with GitHub Issues-based support, and users are instead encouraged to become active participants in its continued development — by fixing any bugs that they encounter, or improving the documentation wherever it’s found to be lacking. 47 | 48 | If you wish to make a change, [open a Pull Request](https://github.com/alexito4/EquallySpacedStack/pull/new) — even if it just contains a draft of the changes you’re planning, or a test that reproduces an issue — and we can discuss it further from there. 49 | 50 | ## Author 51 | 52 | Alejandro Martinez | http://alejandromp.com | [@alexito4](https://twitter.com/alexito4) 53 | -------------------------------------------------------------------------------- /Sources/EquallySpacedStack/EquallySpacedStack.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct EquallySpacedHStack: View { 4 | let items: [AnyView] 5 | 6 | public init(@ViewBuilder content: () -> A) { // this init will be used for any non-supported number of TupleView 7 | let views = content() 8 | self.items = [AnyView(views)] 9 | } 10 | 11 | // MARK: TupleView support 12 | 13 | public init(@ViewBuilder content: () -> TupleView<(A, B)>) { 14 | let views = content().value 15 | self.items = [AnyView(views.0), AnyView(views.1)] 16 | } 17 | 18 | public init(@ViewBuilder content: () -> TupleView<(A, B, C)>) { 19 | let views = content().value 20 | self.items = [AnyView(views.0), AnyView(views.1), AnyView(views.2)] 21 | } 22 | 23 | public init(@ViewBuilder content: () -> TupleView<(A, B, C, D)>) { 24 | let views = content().value 25 | self.items = [AnyView(views.0), AnyView(views.1), AnyView(views.2), AnyView(views.3)] 26 | } 27 | 28 | // MARK: ForEach support 29 | 30 | public init(@ViewBuilder content: () -> ForEach) { 31 | let views = content() 32 | self.items = views.data.map({ AnyView(views.content($0)) }) 33 | } 34 | 35 | public var body: some View { 36 | HStack { 37 | Spacer() 38 | ForEach(0..(@ViewBuilder content: () -> A) { // this init will be used for any non-supported number of TupleView 50 | self.items = [AnyView(content())] 51 | } 52 | 53 | // MARK: TupleView support 54 | 55 | public init(@ViewBuilder content: () -> TupleView<(A, B)>) { 56 | let views = content().value 57 | self.items = [AnyView(views.0), AnyView(views.1)] 58 | } 59 | 60 | public init(@ViewBuilder content: () -> TupleView<(A, B, C)>) { 61 | let views = content().value 62 | self.items = [AnyView(views.0), AnyView(views.1), AnyView(views.2)] 63 | } 64 | 65 | public init(@ViewBuilder content: () -> TupleView<(A, B, C, D)>) { 66 | let views = content().value 67 | self.items = [AnyView(views.0), AnyView(views.1), AnyView(views.2), AnyView(views.3)] 68 | } 69 | 70 | // MARK: ForEach support 71 | 72 | public init(@ViewBuilder content: () -> ForEach) { 73 | let views = content() 74 | self.items = views.data.map({ AnyView(views.content($0)) }) 75 | } 76 | 77 | public var body: some View { 78 | VStack { 79 | Spacer() 80 | ForEach(0.. [XCTestCaseEntry] { 5 | return [ 6 | testCase(EquallySpacedStackTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import EquallySpacedStackTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += EquallySpacedStackTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /distributed_stack-aaae1c88c4412c44c53045f94508be1c-103f7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexito4/EquallySpacedStack/4de61d935c784e549425c79341915ee2eda78107/distributed_stack-aaae1c88c4412c44c53045f94508be1c-103f7.png --------------------------------------------------------------------------------