Last active
November 3, 2021 08:33
-
-
Save aziyan99/84d0a869e78138ccf4c0a26d8b013098 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 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