Is it possible to pass the values of GeometryReader to a #Observableobject - swift

I need to do calculations based on the size of the device and the width of a screen.
struct TranslatorView: View {
#ObservedObject var settings = TranslationViewModel(spacing: 4, charSize: 20)
var body: some View {
GeometryReader { geometry in
VStack{
TextField("Enter your name", text:self.$settings.translateString)
}
}
}
}
My ObservableObject can be seen below
class TranslationViewModel: ObservableObject {
#Published var translateString = ""
var ScreenSize : CGFloat = 0
var spacing : CGFloat = 4
var charSize : CGFloat = 20
init(spacing: CGFloat, charSize : CGFloat) {
self.spacing = spacing
self.charSize = charSize
}
}
I need a way to pass the geometry.size.width to my ScreenSize property but have no idea how to do this.

The simplest way is to have setter-method inside the ObservableObject which returns an EmptyView.
import SwiftUI
struct TranslatorView: View {
#ObservedObject var settings = TranslationViewModel(spacing: 4, charSize: 20)
var body: some View {
GeometryReader { geometry in
VStack{
self.settings.passWidth(geometry: geometry)
TextField("Enter your name", text:self.$settings.translateString)
}
}
}
}
class TranslationViewModel: ObservableObject {
#Published var translateString = ""
var ScreenSize : CGFloat = 0
var spacing : CGFloat = 4
var charSize : CGFloat = 20
init(spacing: CGFloat, charSize : CGFloat) {
self.spacing = spacing
self.charSize = charSize
}
func passWidth(geometry: GeometryProxy) -> EmptyView {
self.ScreenSize = geometry.size.width
return EmptyView()
}
}
Then you could implement a wrapper around GeometryReader taking content: () -> Content and a closure which gets executed every time the GeometryReader gets rerendered where you can update whatever you wish.
import SwiftUI
struct TranslatorView: View {
#ObservedObject var settings = TranslationViewModel(spacing: 4, charSize: 20)
var body: some View {
GeometryReaderEasy(callback: {
self.settings.ScreenSize = $0.size.width
}) { geometry in
TextField("Enter your name", text:self.$settings.translateString)
}
}
}
struct GeometryReaderEasy<Content: View>: View {
var callback: (GeometryProxy) -> ()
var content: (GeometryProxy) -> (Content)
private func setGeometry(geometry: GeometryProxy) -> EmptyView {
callback(geometry)
return EmptyView()
}
var body: some View {
GeometryReader { geometry in
VStack{
self.setGeometry(geometry: geometry)
self.content(geometry)
}
}
}
}

You can use a simple extension on View to allow arbitrary code execution when building your views.
extension View {
func execute(_ closure: () -> Void) -> Self {
closure()
return self
}
}
And then
var body: some View {
GeometryReader { proxy
Color.clear.execute {
self.myObject.useProxy(proxy)
}
}
}

Another option is to set the value using .onAppear
struct TranslatorView: View {
#ObservedObject var settings = TranslationViewModel(spacing: 4, charSize: 20)
var body: some View {
GeometryReader { geometry in
VStack{
TextField("Enter your name", text:self.$settings.translateString)
} .onAppear {
settings.ScreenSize = geometry.size.width
}
}
}
}

Related

Make sheet the exact size of the content inside

Let's say I have a custom view inside of a sheet, something like this
VStack {
Text("Title")
Text("Some very long text ...")
}
.padding()
.presentationDetents([.height(250)])
How can I get the exact height of the VStack and pass it to the presentationDetents modifier so that the height of the sheet is exactly the height of the content inside?
You can use a GeometryReader and PreferenceKey to read the size and then write it to a state variable. In my example, I store the entire size, but you could adjust it to store just the height, since it's likely that that is the only parameter you need.
struct ContentView: View {
#State private var showSheet = false
#State private var size: CGSize = .zero
var body: some View {
Button("View sheet") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
VStack {
Text("Title")
Text("Some very long text ...")
}
.padding()
.background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self) { newSize in
size.height = newSize.height
}
.presentationDetents([.height(size.height)])
}
}
}
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() }
}
Using the general idea made by #jnpdx including some updates such as reading the size of the overlay instead of the background, here is what works for me:
struct ContentView: View {
#State private var showSheet = false
#State private var sheetHeight: CGFloat = .zero
var body: some View {
Button("Open sheet") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
VStack {
Text("Title")
Text("Some very long text ...")
}
.padding()
.overlay {
GeometryReader { geometry in
Color.clear.preference(key: InnerHeightPreferenceKey.self, value: geometry.size.height)
}
}
.onPreferenceChange(InnerHeightPreferenceKey.self) { newHeight in
sheetHeight = newHeight
}
.presentationDetents([.height(sheetHeight)])
}
}
}
struct InnerHeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
More reuseable
struct InnerHeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() }
}
extension View {
func fixedInnerHeight(_ sheetHeight: Binding<CGFloat>) -> some View {
padding()
.background {
GeometryReader { proxy in
Color.clear.preference(key: InnerHeightPreferenceKey.self, value: proxy.size.height)
}
}
.onPreferenceChange(InnerHeightPreferenceKey.self) { newHeight in sheetHeight.wrappedValue = newHeight }
.presentationDetents([.height(sheetHeight.wrappedValue)])
}
}
struct ExampleView: View {
#State private var showSheet = false
#State private var sheetHeight: CGFloat = .zero
var body: some View {
Button("Open sheet") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
VStack {
Text("Title")
Text("Some very long text ...")
}
.fixedInnerHeight($sheetHeight)
}
}
}
struct ContentView: View {
#State private var showingSheet = false
let heights = stride(from: 0.1, through: 1.0, by: 0.1).map { PresentationDetent.fraction($0) }
var body: some View {
Button("Show Sheet") {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
Text("Random text ")
.presentationDetents(Set(heights))
}
}
}

How can I make a Custom ScrollView without using SwiftUI ScrollView?

I'm currently working on Custom ScrollView without ScrollView.
I don't want to add too many features, but I want to be able to scroll by dragging.
I've searched the Internet, but all I can find are examples of wrapped ScrollView.
The code I'm working on is as follows(WIP code):
import SwiftUI
struct ContentView: View {
#State private var yOffset: CGFloat = 0
#State private var contentSize: CGSize = .zero
var body: some View {
CustomScrollView {
ForEach(0..<100) { i in
Text(
"\(i)"
)
.frame(
maxWidth: .infinity
)
.background(
Color.green
)
}
.offset(y: 0)
.size(size: $contentSize)
}
.offset(y: 0)
}
}
struct CustomScrollView<Content: View>: View {
let content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
self.content
}
}
//==================================================
// For extension
//==================================================
struct ChildSizeReader<Content: View>: View {
#Binding var size: CGSize
let content: () -> Content
var body: some View {
// Remove ZStack from the existing answer.
content().background(
GeometryReader { proxy in
Color.clear.preference(
key: SizePreferenceKey.self,
value: proxy.size
)
}
)
.onPreferenceChange(SizePreferenceKey.self) { preferences in
self.size = preferences
}
}
}
struct SizePreferenceKey: PreferenceKey {
typealias Value = CGSize
static var defaultValue: Value = .zero
static func reduce(value _: inout Value, nextValue: () -> Value) {
_ = nextValue()
}
}
extension View {
func size(size: Binding<CGSize>) -> some View {
ChildSizeReader(size: size) {
self
}
}
}
As you can see in the attached image, the top of the content is missing.
I would like to adjust it so that the top is visible, but I don't know how to do that.
Note:
The dragging and other processes will be implemented after the top is visible first.
Reference:
https://www.youtube.com/watch?v=vuFUX1Qwrmo
SwiftUI - Get size of child?

Placing SwiftUI Data Sources Somewhere Else

I'm trying to use SwiftUI in a project but beyond the very basic version of using #States and #Bindings that can be found in every tutorial, so I need some help on what I'm doing wrong here.
Environment Setup:
I have following files involved with this problem:
CustomTextField: It's a SwiftUI View that contains an internal TextField along with bunch of other things (According to the design)
CustomTextFieldConfiguration: Contains the things that I need to configure on my custom textfield view
RootView: It's a SwiftUI View that is using CustomTextField as one of it's subviews
RootPresenter: This is where the UI Logic & Presentation Logic goes (Between the view and business logic)
RootPresentationModel: It's the viewModel through which the Presenter can modify view's state
RootBuilder: It contains the builder class that uses the builder pattern to wire components together
The Problem:
The textField value does not update in the textValue property of rootPresentationModel
Here are the implementations (Partially) as I have done and have no idea where I have gone wrong:
CustomTextField:
struct CustomTextField: View {
#Binding var config: CustomTextFieldConfiguration
var body: some View {
ZStack {
VStack {
VStack {
ZStack {
HStack {
TextField($config.placeHolder,
value: $config.textValue,
formatter: NumberFormatter(),
onEditingChanged: {_ in },
onCommit: {})
.frame(height: 52.0)
.padding(EdgeInsets(top: 0, leading: 16 + ($config.detailActionImage != nil ? 44 : 0),
bottom: 0, trailing: 16 + ($config.contentAlignment == .center && $config.detailActionImage != nil ? 44 : 0)))
.background($config.backgroundColor)
.cornerRadius($config.cornerRedius)
.font($config.font)
...
...
...
...
CustomTextFieldConfiguration:
struct CustomTextFieldConfiguration {
#Binding var textValue: String
...
...
...
...
RootView:
struct RootView: View {
#State var configuration: CustomTextFieldConfiguration
var interactor: RootInteractorProtocol!
#Environment(\.colorScheme) private var colorScheme
var body: some View {
HStack {
Spacer(minLength: 40)
VStack(alignment: .trailing) {
CustomTextField(config: $configuration)
Text("\(configuration.textValue)")
}
Spacer(minLength: 40)
}
}
}
RootPresenter:
class RootPresenter: BasePresenter {
#ObservedObject var rootPresentationModel: RootPresentationModel
init(presentationModel: RootPresentationModel) {
rootPresentationModel = presentationModel
}
...
...
...
RootPresentationModel:
class RootPresentationModel: ObservableObject {
var textValue: String = "" {
didSet {
print(textValue)
}
}
}
RootBuilder:
class RootBuilder: BaseBuilder {
class func build() -> (RootView, RootInteractor) {
let interactor = RootInteractor()
let presenter = RootPresenter(presentationModel: RootPresentationModel())
let view: RootView = RootView(configuration: CustomTextFieldConfiguration.Presets.priceInput(textValue: presenter.$rootPresentationModel.textValue, placeholder: "", description: ""), interactor: interactor)
let router = RootRouter()
interactor.presenter = presenter
interactor.router = router
return (view, interactor)
}
}
(That Presets method doesn't do anything important, but just to make sure it will not raise an irrelevant question, here's the implementation):
static func priceInput(textValue: Binding<String>, placeholder: String, description: String) -> CustomTextFieldConfiguration {
return CustomTextFieldConfiguration(textValue: textValue,
placeHolder: placeholder,
description: description,
defaultDescription: description,
textAlignment: .center,
descriptionAlignment: .center,
contentAlignment: .center,
font: CustomFont.headline1))
}
import SwiftUI
struct CustomTextField: View {
#EnvironmentObject var config: CustomTextFieldConfiguration
#Binding var textValue: Double
var body: some View {
ZStack {
VStack {
VStack {
ZStack {
HStack {
//Number formatter forces the need for Double
TextField(config.placeHolder,
value: $textValue,
formatter: NumberFormatter(),
onEditingChanged: {_ in },
onCommit: {})
.frame(height: 52.0)
//.padding(EdgeInsets(top: 0, leading: 16 + (Image(systemName: config.detailActionImageName) != nil ? 44 : 0),bottom: 0, trailing: 16 + (config.contentAlignment == .center && Image(systemName: config.detailActionImageName) != nil ? 44 : 0)))
.background(config.backgroundColor)
.cornerRadius(config.cornerRedius)
.font(config.font)
}
}
}
}
}
}
}
class CustomTextFieldConfiguration: ObservableObject {
#Published var placeHolder: String = "place"
#Published var detailActionImageName: String = "checkmark"
#Published var contentAlignment: UnitPoint = .center
#Published var backgroundColor: Color = Color(UIColor.secondarySystemBackground)
#Published var font: Font = .body
#Published var cornerRedius: CGFloat = CGFloat(5)
#Published var description: String = ""
#Published var defaultDescription: String = ""
#Published var textAlignment: UnitPoint = .center
#Published var descriptionAlignment: UnitPoint = .center
init() {
}
init(placeHolder: String, description: String, defaultDescription: String, textAlignment: UnitPoint,descriptionAlignment: UnitPoint,contentAlignment: UnitPoint, font:Font) {
self.placeHolder = placeHolder
self.description = description
self.defaultDescription = defaultDescription
self.textAlignment = textAlignment
self.descriptionAlignment = descriptionAlignment
self.contentAlignment = contentAlignment
self.font = font
}
struct Presets {
static func priceInput(placeholder: String, description: String) -> CustomTextFieldConfiguration {
return CustomTextFieldConfiguration(placeHolder: placeholder, description: description,defaultDescription: description,textAlignment: .center,descriptionAlignment: .center,contentAlignment: .center, font:Font.headline)
}
}
}
struct RootView: View {
#ObservedObject var configuration: CustomTextFieldConfiguration
//var interactor: RootInteractorProtocol!
#Environment(\.colorScheme) private var colorScheme
#Binding var textValue: Double
var body: some View {
HStack {
Spacer(minLength: 40)
VStack(alignment: .trailing) {
CustomTextField(textValue: $textValue).environmentObject(configuration)
Text("\(textValue)")
}
Spacer(minLength: 40)
}
}
}
//RootPresenter is a class #ObservedObject only works properly in SwiftUI Views/struct
class RootPresenter//: BasePresenter
{
//Won't work can't chain ObservableObjects
// var rootPresentationModel: RootPresentationModel
//
// init(presentationModel: RootPresentationModel) {
// rootPresentationModel = presentationModel
// }
}
class RootPresentationModel: ObservableObject {
#Published var textValue: Double = 12 {
didSet {
print(textValue)
}
}
}
struct NewView: View {
//Must be observed directly
#StateObject var vm: RootPresentationModel = RootPresentationModel()
//This cannot be Observed
let presenter: RootPresenter = RootPresenter()
var body: some View {
RootView(configuration: CustomTextFieldConfiguration.Presets.priceInput(placeholder: "", description: ""), textValue: $vm.textValue//, interactor: interactor
)
}
}
struct NewView_Previews: PreviewProvider {
static var previews: some View {
NewView()
}
}

A view extension that runs conditional code based on its GeometryReader results

I’ve created a View extension to read its offset (inspired by https://fivestars.blog/swiftui/swiftui-share-layout-information.html):
func readOffset(in coordinateSpace: String? = nil, onChange: #escaping (CGFloat) -> Void) -> some View {
background(
GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self,
value: -$0.frame(in: coordinateSpace == nil ? .global : .named(coordinateSpace)).origin.y)
})
.onPreferenceChange(ViewOffsetKey.self, perform: onChange)
}
I’m also using Federico’s readSize function:
func readSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geo in
Color.clear
.preference(key: SizePreferenceKey.self, value: geo.size)
})
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
The two work together to help me determine whether a child view within a scrollview is on/off-screen:
struct TestInfinityList: View {
#State var visibleItems: Set<Int> = []
#State var items: [Int] = Array(0...20)
#State var size: CGSize = .zero
var body: some View {
ScrollView(.vertical) {
ForEach(items, id: \.self) { item in
GeometryReader { geo in
VStack {
Text("Item \(item)")
}.id(item)
.readOffset(in: "scroll") { newOffset in
if !isOffscreen(when: newOffset, in: size.height) {
visibleItems.insert(item)
}
else {
visibleItems.remove(item)
}
}
}.frame(height: 300)
}
}.coordinateSpace(name: "scroll")
}
.readSize { newSize in
self.size = newSize
}
}
This is the isOffscreen function that checks for visibility:
func isOffscreen(when offset: CGFloat, in height: CGFloat) -> Bool {
if offset <= 0 && offset + height >= 0 {
return false
}
return true
}
Everything works fine. However, I’d like to optimise the code further into a single extension that checks for visibility based on the offset and size.height inputted, and also receives parameters for what to do if visible and when not i.e. move readOffset’s closure to be logic that co-exists with the extension code.
I’ve no idea whether this is feasible but thought it’s worth an ask.
You just need to create a View or ViewModifier that demands some Bindings. Note, the code below is just an example of some of the patterns you can use (e.g., an optional binding, escaping content closure), but in the form of a Stack style wrap rather than a ViewModifier (which based on the blog you know how to setup).
struct ScrollableVStack<Content: View>: View {
let content: Content
#Binding var useScrollView: Bool
#Binding var scroller: ScrollViewProxy?
#State private var staticGeo = ViewGeometry()
#State private var scrollContainerGeo = ViewGeometry()
let topFade: CGFloat
let bottomFade: CGFloat
init(_ useScrollView: Binding<Bool>,
topFade: CGFloat = 0.09,
bottomFade: CGFloat = 0.09,
_ scroller: Binding<ScrollViewProxy?> = .constant(nil),
#ViewBuilder _ content: #escaping () -> Content ) {
_useScrollView = useScrollView
_scroller = scroller
self.content = content()
self.topFade = topFade
self.bottomFade = bottomFade
}
var body: some View {
if useScrollView { scrollView }
else { VStack { staticContent } }
}
var scrollView: some View {
ScrollViewReader { scroller in
ScrollView(.vertical, showsIndicators: false) {
staticContent
.onAppear { self.scroller = scroller }
}
.geometry($scrollContainerGeo)
.fadeInOut(topFade: staticGeo.size.height * topFade,
bottomFade: staticGeo.size.height * bottomFade)
}
.onChange(of: staticGeo.size.height) { newStaticHeight in
useScrollView = newStaticHeight > scrollContainerGeo.size.height * 0.85
}
}
var staticContent: some View {
content
.geometry($staticGeo)
.padding(.top, staticGeo.size.height * topFade * 1.25)
.padding(.bottom, staticGeo.size.height * bottomFade)
}
}

Passing an EnvronmentObject to NSHostingControllers

I'm using a custom SwiftUI View using NSSplitViewController that takes in ViewBuilders for the two subviews. My problem is any change in state of the environment doesn't propagate to the subviews inside SplitView, but propagates to another TextView in ContentView
import SwiftUI
class AppEnvironment : ObservableObject {
#Published var value: String = "default"
}
struct ContentView: View {
#EnvironmentObject var env : AppEnvironment
var body: some View {
HStack {
Button(action: {
self.env.value = "new value"
}, label: { Text("Change value") })
Text(self.env.value)
GeometryReader { geometry in
SplitView(master: {
Text("master")
.background(Color.yellow)
}, detail: {
HStack {
Text(self.env.value) }
.background(Color.orange)
}).frame(width: geometry.size.width, height: geometry.size.height)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
//
// Source: https://gist.github.com/HashNuke/f8895192fff1f275e66c30340f304d80
//
struct SplitView<Master: View, Detail: View>: View {
var master: Master
var detail: Detail
init(#ViewBuilder master: () -> Master, #ViewBuilder detail: () -> Detail) {
self.master = master()
self.detail = detail()
}
var body: some View {
let viewControllers = [NSHostingController(rootView: master), NSHostingController(rootView: detail)]
return SplitViewController(viewControllers: viewControllers)
}
}
struct SplitViewController: NSViewControllerRepresentable {
var viewControllers: [NSViewController]
private let splitViewResorationIdentifier = "com.company.restorationId:mainSplitViewController"
func makeNSViewController(context: Context) -> NSViewController {
let controller = NSSplitViewController()
controller.splitView.dividerStyle = .thin
controller.splitView.autosaveName = NSSplitView.AutosaveName(splitViewResorationIdentifier)
controller.splitView.identifier = NSUserInterfaceItemIdentifier(rawValue: splitViewResorationIdentifier)
let vcLeft = viewControllers[0]
let vcRight = viewControllers[1]
vcLeft.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
vcRight.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 70).isActive = true
let sidebarItem = NSSplitViewItem(contentListWithViewController: vcLeft)
sidebarItem.canCollapse = false
// I'm not sure if this has any impact
// controller.view.frame = CGRect(origin: .zero, size: CGSize(width: 800, height: 800))
controller.addSplitViewItem(sidebarItem)
let mainItem = NSSplitViewItem(viewController: vcRight)
controller.addSplitViewItem(mainItem)
return controller
}
func updateNSViewController(_ nsViewController: NSViewController, context: Context) {
print("should update splitView")
}
}
Yes, in such case EnvironmentObject is not injected automatically. The solution would be to separate content into designated views (for better design) and inject environment object manually.
Here it is
Text(self.env.value)
GeometryReader { geometry in
SplitView(master: {
MasterView().environmentObject(self.env)
}, detail: {
HStack {
DetailView().environmentObject(self.env)
}).frame(width: geometry.size.width, height: geometry.size.height)
}
and views
struct MasterView: View {
var body: some View {
Text("master")
.background(Color.yellow)
}
}
struct DetailView: View {
var body: some View {
HStack {
Text(self.env.value) }
.background(Color.orange)
}
}