Skip to content

Commit

Permalink
Existing Code moved from private project repo
Browse files Browse the repository at this point in the history
  • Loading branch information
diniska authored May 25, 2021
2 parents ab9be4c + 8daeb0b commit 85b076d
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 2 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Build & Test

on:
push:
branches: [ void ]
pull_request:
branches: [ void ]

jobs:
build:

runs-on: macos-latest

steps:
- uses: actions/checkout@v2
- name: Build for macOS
run: swift build -v
- name: Run tests for macOS
run: swift test -v
- name: Build for iOS
run: xcodebuild build -sdk iphoneos -scheme 'WrappingStack'
- name: Run tests for iOS
run: xcodebuild test -destination 'name=iPhone 8' -scheme "WrappingStack"
7 changes: 7 additions & 0 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "WrappingStack",
platforms: [
.iOS(.v9),
.watchOS(.v6),
.tvOS(.v13),
.macOS(.v10_10)
],
products: [
.library(
name: "WrappingStack",
targets: ["WrappingStack"]),
],
targets: [
.target(
name: "WrappingStack",
dependencies: []),
.testTarget(
name: "WrappingStackTests",
dependencies: ["WrappingStack"]),
]
)
2 changes: 0 additions & 2 deletions README.md

This file was deleted.

7 changes: 7 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Swiftui WrappingStack

![Swift 5.3](https://img.shields.io/badge/Swift-5.3-FA5B2C) ![Xcode 12.5](https://img.shields.io/badge/Xcode-12-44B3F6) ![iOS 9.0](https://img.shields.io/badge/iOS-8.0-178DF6) ![iPadOS 9.0](https://img.shields.io/badge/iPadOS-8.0-178DF6) ![MacOS 10.10](https://img.shields.io/badge/MacOS-10.10-178DF6) [![Build & Test](https://github.com/diniska/swiftui-wrapping-stack/actions/workflows/test.yml/badge.svg)](https://github.com/diniska/swiftui-wrapping-stack/actions/workflows/test.yml)

A SwiftUI Views for wrapping HStack elements into multiple lines.

`WrappingHStack` - provides `HStack` that supports line wrapping
34 changes: 34 additions & 0 deletions Sources/WrappingStack/Helpers/SizeReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#if canImport(SwiftUI) && canImport(Combine)

import SwiftUI

@available(iOS 14, macOS 11, *)
extension View {
func onSizeChange(perform action: @escaping (CGSize) -> ()) -> some View {
modifier(SizeReader(onChange: action))
}
}

@available(iOS 14, macOS 11, *)
private struct SizeReader: ViewModifier {
var onChange: (CGSize) -> ()

func body(content: Content) -> some View {
content
.background(
GeometryReader { geometry in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometry.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
}

@available(iOS 14, macOS 11, *)
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}

#endif
28 changes: 28 additions & 0 deletions Sources/WrappingStack/Helpers/TightHeightGeometryReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#if canImport(SwiftUI) && canImport(Combine)

import SwiftUI

@available(iOS 14, macOS 11, *)
struct TightHeightGeometryReader<Content: View>: View {
@State private var height: CGFloat = 0

var content: (GeometryProxy) -> Content

init(@ViewBuilder content: @escaping (GeometryProxy) -> Content) {
self.content = content
}

var body: some View {
GeometryReader { geometry in
content(geometry)
.onSizeChange { size in
if self.height != size.height {
self.height = size.height
}
}
}
.frame(height: height)
}
}

#endif
138 changes: 138 additions & 0 deletions Sources/WrappingStack/WrappingHStack.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#if canImport(SwiftUI) && canImport(Combine)

import SwiftUI

/// An HStack that grows vertically when single line overflows
@available(iOS 14, macOS 11, *)
public struct WrappingHStack<Data: RandomAccessCollection, ID: Hashable, Content: View>: View {

public let data: Data
public var content: (Data.Element) -> Content
public var id: KeyPath<Data.Element, ID>
public var alignment: Alignment
public var horizontalSpacing: CGFloat
public var verticalSpacing: CGFloat

@State private var sizes: [ID: CGSize] = [:]
@State private var calculatesSizesKeys: Set<ID> = []

private let idsForCalculatingSizes: Set<ID>
private var dataForCalculatingSizes: [Data.Element] {
var result: [Data.Element] = []
var idsToProcess: Set<ID> = idsForCalculatingSizes
idsToProcess.subtract(calculatesSizesKeys)

data.forEach { item in
let itemId = item[keyPath: id]
if idsToProcess.contains(itemId) {
idsToProcess.remove(itemId)
result.append(item)
}
}
return result
}

public init(
id: KeyPath<Data.Element, ID>,
alignment: Alignment = .center,
horizontalSpacing: CGFloat = 0,
verticalSpacing: CGFloat = 0,
@ViewBuilder content create: () -> ForEach<Data, ID, Content>
){
let forEach = create()
data = forEach.data
content = forEach.content
idsForCalculatingSizes = Set(data.map { $0[keyPath: id] })
self.id = id
self.alignment = alignment
self.horizontalSpacing = horizontalSpacing
self.verticalSpacing = verticalSpacing
}

private func splitIntoLines(maxWidth: CGFloat) -> [Range<Data.Index>] {
var width: CGFloat = 0
var result: [Range<Data.Index>] = []
var lineStart = data.startIndex
var lineLength = 0

for element in data {
guard let elementWidth = sizes[element[keyPath: id]]?.width
else { break }
let newWidth = width + elementWidth
if newWidth < maxWidth || lineLength == 0 {
width = newWidth + horizontalSpacing
lineLength += 1
} else {
width = elementWidth
let lineEnd = data.index(lineStart, offsetBy:lineLength)
result.append(lineStart ..< lineEnd)
lineLength = 0
lineStart = lineEnd
}
}

if lineStart != data.endIndex {
result.append(lineStart ..< data.endIndex)
}
return result
}

public var body: some View {
if calculatesSizesKeys.isSuperset(of: idsForCalculatingSizes) {
TightHeightGeometryReader { geometry in
let splitted = splitIntoLines(maxWidth: geometry.size.width)

// All sizes are known
VStack(alignment: alignment.horizontal, spacing: verticalSpacing) {
ForEach(Array(splitted.enumerated()), id: \.offset) { list in
HStack(alignment: alignment.vertical, spacing: horizontalSpacing) {
ForEach(data[list.element], id: id) {
content($0)
}
}
}
}
}
} else {
// Calculating sizes
VStack {
ForEach(dataForCalculatingSizes, id: id) { d in
content(d)
.onSizeChange { size in
let key = d[keyPath: id]
sizes[key] = size
calculatesSizesKeys.insert(key)
}
}
}
}
}
}

@available(iOS 14, macOS 11, *)
extension WrappingHStack where ID == Data.Element.ID, Data.Element: Identifiable {
public init(@ViewBuilder content create: () -> ForEach<Data, ID, Content>) {
self.init(id: \.id, content: create)
}
}

#if DEBUG

@available(iOS 14, macOS 11, *)
struct WrappingHStack_Previews: PreviewProvider {
static var previews: some View {
WrappingHStack(id: \.self, alignment: .topLeading) {
ForEach(["Hello1", "world1", "Hello2", "world2", "Hello3", "world3", "Hello4", "world4 ", "Hello1"], id: \.self) { item in
Text(item)
.padding()
.background(Color(.systemGray))
.cornerRadius(3)
}
}
.frame(width: 300)
}
}

#endif

#endif
5 changes: 5 additions & 0 deletions Tests/WrappingStackTests/WrappingStackTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import XCTest
@testable import WrappingStack

final class WrappingStackTests: XCTestCase {
}

0 comments on commit 85b076d

Please sign in to comment.