Instantly share code, notes, and snippets.
Created
July 7, 2022 13:40
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save gromwel/97d754b19ed485484c0e1894394b6a81 to your computer and use it in GitHub Desktop.
Вьюха с расстановкой контента построчно с переносами
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
import Combine | |
extension Int { | |
static func random() -> Int { | |
let lenght = Int.random(in: 1...15) | |
return Array(0...lenght).reduce(into: 0) { result, x in | |
let random = Array(0...9).randomElement() ?? 0 | |
result *= 10 | |
result += random | |
} | |
} | |
} | |
// MARK: - Пример | |
struct ContentView: View { | |
let tags1: [String] = ["hello", "world", "ios", "wwdc", "one more thing", "macos"] | |
@State var tags2: [Int] = [] | |
var body: some View { | |
VStack { | |
ScrollView { | |
TagsView(tags: tags1) { AnyView(TagStringView(text: $0)) } | |
TagsView(tags: tags2) { AnyView(TagIntView(number: $0)) } | |
} | |
Button { | |
tags2.append(Int.random()) | |
} label: { | |
Text("add one") | |
.foregroundColor(.white) | |
.padding() | |
.background(.blue) | |
.cornerRadius(16) | |
} | |
} | |
} | |
} | |
struct TagStringView: View { | |
let text: String | |
var body: some View { | |
Text(text) | |
.font(.body) | |
.lineLimit(1) | |
.padding(8) | |
.background( | |
RoundedRectangle(cornerRadius: 16, style: .continuous) | |
.stroke(Color(UIColor.systemGray6)) | |
) | |
.padding(4) | |
} | |
} | |
struct TagIntView: View { | |
let number: Int | |
var body: some View { | |
Button { | |
print(String(number)) | |
} label: { | |
Text(String(number)) | |
.font(.caption2) | |
.lineLimit(1) | |
.padding(4) | |
.background( | |
RoundedRectangle(cornerRadius: 8, style: .continuous) | |
.fill(Color(UIColor.systemGray6)) | |
) | |
.padding(2) | |
} | |
} | |
} | |
// MARK: - | |
// MARK: Вьюха расстановки "тегов" | |
struct TagsView<T>: View { | |
var tags: [T] | |
var content: (T) -> AnyView | |
@State private var sizesDict: [Int: CGSize] = [:] | |
@State private var totalHeight: CGFloat = 0 | |
var body: some View { | |
// Топ ридер который даст ширину для вычислений | |
GeometryReader { topProxy in | |
// ZStack в который и будем класть все теги, они должны быть на разныех слоях так как положение регулируем отступами | |
ZStack(alignment: .topLeading) { | |
ForEach(Array(0..<tags.count), id: \.self) { index in | |
// Берем ui по значению | |
content(tags[index]) | |
.background( | |
// Ставим бэкграундом вычислитель размера конкретной сабвьюшки | |
SizeCalculator<GPTagK> { | |
// Мы сами определили ключ по которому передавать значение и тут маппим это значение | |
[GPKModel(id: index, size: $0.size)] | |
} | |
) | |
// Рассчитаные отспупы для каждой вьюшки | |
.padding(.top, sizesDict[index]?.height) | |
.padding(.leading, sizesDict[index]?.width) | |
} | |
} | |
//.background(.purple) | |
.background( | |
// Ставим бекграундом вычислитель размера всего стека с вьюшками | |
SizeCalculator<GPReaderK> { [GPKModel(id: NSNotFound, size: $0.size)] } | |
) | |
// Следим за изменением значения по ключу каждой вьюхи | |
.onPreferenceChange(GPTagK.self) { value in | |
var x: CGFloat = 0 | |
var y: CGFloat = 0 | |
// Все рассчеты для расстановки тегов тут | |
value.forEach { model in | |
var currentX = x | |
var currentY = y | |
if x + model.size.width > topProxy.size.width { | |
x = model.size.width | |
y += model.size.height | |
currentX = 0 | |
currentY = y | |
} else { | |
x += model.size.width | |
} | |
// рассчитанные отступы для каждой вьюхи кладем в словарь | |
self.sizesDict[model.id] = CGSize(width: currentX, height: currentY) | |
} | |
} | |
} | |
// Установка общей высоты для тор ридера, иначе он займер все пространство | |
.frame(height: totalHeight) | |
// Следим за изменением значения по ключу общей высоты | |
.onPreferenceChange(GPReaderK.self) { value in | |
// надо проверить не пустое ли значение, может крашнуть | |
totalHeight = value[0].size.height | |
} | |
} | |
} | |
// MARK: Подложка которая посчитает размеры вьюхи и передаст "наверх" | |
// можно передать разные ключи и обработав proxy передать необходимые параметры | |
struct SizeCalculator<Key: PreferenceKey>: View { | |
let value: (GeometryProxy) -> Key.Value | |
var body: some View { | |
GeometryReader { proxy in | |
Color.clear | |
.preference(key: Key.self,value: value(proxy)) | |
} | |
} | |
} | |
// MARK: Preference key | |
struct GPKModel: Equatable { | |
let id: Int | |
let size: CGSize | |
} | |
struct GPTagK: PreferenceKey { | |
static var defaultValue: [GPKModel] = [] | |
static func reduce(value: inout [GPKModel], nextValue: () -> [GPKModel]) { | |
value.append(contentsOf: nextValue()) | |
} | |
} | |
struct GPReaderK: PreferenceKey { | |
static var defaultValue: [GPKModel] = [] | |
static func reduce(value: inout [GPKModel], nextValue: () -> [GPKModel]) { | |
value.append(contentsOf: nextValue()) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment