Skip to content

Instantly share code, notes, and snippets.

@aziyan99
Last active November 3, 2021 08:33
Show Gist options
  • Save aziyan99/84d0a869e78138ccf4c0a26d8b013098 to your computer and use it in GitHub Desktop.
Save aziyan99/84d0a869e78138ccf4c0a26d8b013098 to your computer and use it in GitHub Desktop.
import SwiftUI
import NaturalLanguage
struct ContentView: View {
@ObservedObject var viewModel = ViewModel()
var body: some View {
MultilineHStack(self.viewModel.words) {
TextViewCustom(str: $0.word)
}
}
}
public struct MultilineHStack: View {
struct SizePreferenceKey: PreferenceKey {
typealias Value = [CGSize]
static var defaultValue: Value = []
static func reduce(value: inout Value, nextValue: () -> Value) {
value.append(contentsOf: nextValue())
}
}
private let items: [AnyView]
@State private var sizes: [CGSize] = []
public init<Data: RandomAccessCollection, Content: View>(_ data: Data, @ViewBuilder content: (Data.Element) -> Content) {
self.items = data.map { AnyView(content($0)) }
}
public var body: some View {
GeometryReader {geometry in
ZStack(alignment: .topLeading) {
ForEach(0..<self.items.count) { index in
self.items[index].background(self.backgroundView()).offset(self.getOffset(at: index, geometry: geometry))
}
}
}.onPreferenceChange(SizePreferenceKey.self) {
self.sizes = $0
}
}
private func getOffset(at index: Int, geometry: GeometryProxy) -> CGSize {
guard index < sizes.endIndex else {return .zero}
let frame = sizes[index]
var (x,y,maxHeight) = sizes[..<index].reduce((CGFloat.zero,CGFloat.zero,CGFloat.zero)) {
var (x,y,maxHeight) = $0
x += $1.width
if x > geometry.size.width {
x = $1.width
y += maxHeight
maxHeight = 0
}
maxHeight = max(maxHeight, $1.height)
return (x,y,maxHeight)
}
if x + frame.width > geometry.size.width {
x = 0
y += maxHeight
}
return .init(width: x, height: y)
}
private func backgroundView() -> some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.clear)
.preference(
key: SizePreferenceKey.self,
value: [geometry.frame(in: CoordinateSpace.global).size]
)
}
}
}
struct TextViewCustom: View {
var str: String = "-"
@State var open = false
@State var popoverSize = CGSize(width: 300, height: 300)
init(str: String) {
self.str = str
}
var body: some View {
WithPopover(
showPopover: $open,
popoverSize: popoverSize,
content: {
Button(action: { self.open.toggle() }) {
Text("\(str) ")
}
},
popoverContent: {
VStack {
Button(action: { self.popoverSize = CGSize(width: 300, height: 600)}) {
Text("\(str)")
}
Button(action: { self.open = false}) {
Text("Close")
}
}
})
}
}
struct Word {
var id: Int
var word: String
}
class ViewModel: ObservableObject {
var raw_text: String = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non vestibulum orci. Ut molestie non tortor at ultrices"
@Published var words = [Word]()
init() {
var id = 1
let tokenizer = NLTokenizer(unit: .word)
tokenizer.string = raw_text
tokenizer.enumerateTokens(in: raw_text.startIndex..<raw_text.endIndex) { tokenRange, _ in
words.append(
Word(id: id, word: "\(raw_text[tokenRange])")
)
id = id + 1
return true
}
}
}
struct WithPopover<Content: View, PopoverContent: View>: View {
@Binding var showPopover: Bool
var popoverSize: CGSize? = nil
let content: () -> Content
let popoverContent: () -> PopoverContent
var body: some View {
content()
.background(
Wrapper(showPopover: $showPopover, popoverSize: popoverSize, popoverContent: popoverContent)
.frame(maxWidth: .infinity, maxHeight: .infinity)
)
}
struct Wrapper<PopoverContent: View> : UIViewControllerRepresentable {
@Binding var showPopover: Bool
let popoverSize: CGSize?
let popoverContent: () -> PopoverContent
func makeUIViewController(context: UIViewControllerRepresentableContext<Wrapper<PopoverContent>>) -> WrapperViewController<PopoverContent> {
return WrapperViewController(
popoverSize: popoverSize,
popoverContent: popoverContent) {
self.showPopover = false
}
}
func updateUIViewController(_ uiViewController: WrapperViewController<PopoverContent>,
context: UIViewControllerRepresentableContext<Wrapper<PopoverContent>>) {
uiViewController.updateSize(popoverSize)
if showPopover {
uiViewController.showPopover()
}
else {
uiViewController.hidePopover()
}
}
}
class WrapperViewController<PopoverContent: View>: UIViewController, UIPopoverPresentationControllerDelegate {
var popoverSize: CGSize?
let popoverContent: () -> PopoverContent
let onDismiss: () -> Void
var popoverVC: UIViewController?
required init?(coder: NSCoder) { fatalError("") }
init(popoverSize: CGSize?,
popoverContent: @escaping () -> PopoverContent,
onDismiss: @escaping() -> Void) {
self.popoverSize = popoverSize
self.popoverContent = popoverContent
self.onDismiss = onDismiss
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
}
func showPopover() {
guard popoverVC == nil else { return }
let vc = UIHostingController(rootView: popoverContent())
if let size = popoverSize { vc.preferredContentSize = size }
vc.modalPresentationStyle = UIModalPresentationStyle.popover
if let popover = vc.popoverPresentationController {
popover.sourceView = view
popover.delegate = self
}
popoverVC = vc
self.present(vc, animated: true, completion: nil)
}
func hidePopover() {
guard let vc = popoverVC, !vc.isBeingDismissed else { return }
vc.dismiss(animated: true, completion: nil)
popoverVC = nil
}
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
popoverVC = nil
self.onDismiss()
}
func updateSize(_ size: CGSize?) {
self.popoverSize = size
if let vc = popoverVC, let size = size {
vc.preferredContentSize = size
}
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none // this is what forces popovers on iPhone
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment