16

I'm using a drag gesture to change the Hue/Saturation of a Color object. The idea is that you can drag across the screen and see all Hue values (0.0 - 1.0), and the same top to bottom with Saturation.

I require the size of the Screen (or view, this is a single view app) in order to normalize/convert the CGPoint values into a range between 0.0 - 1.0, but I am unable to find anyway to get this information. There are many threads discussing this, but they usually talk about how to set the width/heigh of a View when I just want to retrieve it.

I have everything functioning, just that I am using hardcoded values to normalize it.

0

4 Answers 4

21

Look into GeometryReader. It allows you to get the size of the current view:

struct ContentView: View {
    
    var body: some View {
        GeometryReader { geo in
            VStack {
                Text("Hello World!")
                Text("Size:  (\(geo.size.width), \(geo.size.height))")
            }
        }
    }
}

And here is an example where you can drag on the screen to change the hue value:

struct ContentView: View {

    @State private var hue: CGFloat = 0
    
    var body: some View {
        GeometryReader { geo in
            ZStack {
                Color.white
                    .gesture(DragGesture().onChanged({ value in
                        self.hue = value.location.y / geo.size.height
                    }))
                
                VStack {
                    Text("Hello World!")
                    Text("Size:  (\(geo.size.width), \(geo.size.height))")
                    Text("Hue:  \(self.hue)")
                }.allowsHitTesting(false)
            }
        }
        .edgesIgnoringSafeArea(.all)
    }
}

GeometryReader breaking your view layout? Check out this gist.

Sign up to request clarification or add additional context in comments.

5 Comments

Brilliant! BTW, change "Color.white" to "Color(hue: Double(self.hue), saturation: 1.0, brightness: 1.0)" and watch the background color change as you drag up and down!
@George , I tried out your GeometryReaderModifier, which looks like just what I need, but the GeometryProxy reference passed into the closure is always nil. Are there certain types of views that it does and doesn't work on?
@GrahamLea Hmm, not sure what the issue would be. I don't know if greedy views like Spacer or Divider are potentially the issue? The preference keys aren't propagating, so maybe you use UIKit in between? Or maybe try attach the onPreferenceChange after the GeometryReader { ... } instead of .background(...)? Let me know if that helps / what your usage is like.
This doesn't work if the size of the view is larger than the size of the screen/window. What can I do in that case? Once some of the sub-content is larger than the window, the GeometryReader only reports the size of the view which no longer matches the size of the window.
@Kudit Most likely I would think is that the view in question actually isn't larger than the window size?
8

Just for completeness, the GeometryReader will return size of the container. As the OP explicitly referred to screen dimensions it may be worth to mention UIScreen class. We can use UIScreen.main.bounds.width and UIScreen.main.bounds.height to obtain screen dimensions without GeometryReader. It may be worth adding that GeometryReader results reflect size of the container, that will differ from what UIScreen returns

Example

import SwiftUI

struct ContentView: View {
    var body: some View {
        GeometryReader {geometry in
            VStack {
                Text("Sizes")
                    .font(.title)
                VStack {
                    Text("Screen width via UIScreen: \(Int(UIScreen.main.bounds.width))")
                    Text("Screen height via UIScreen: \(Int(UIScreen.main.bounds.height))")
                    Text("This VStack height: \(Int(geometry.size.height))")
                    Text("This VStack width: \(Int(geometry.size.width))")
                }
            }
        }
    }
}

Results

Screen sizes

1 Comment

Unfortunately this solution breaks main SwiftUI benefit (cross-platform) as works directly with UIKit.
5

I'm late to answer this question, but in case anyone is coming here searching for "how to get the height and width of the device screen in Swiftui to work on multiplatform" like me! then this answer may be helpful. I solved my needs by making a class to return the device screen width and height working on multiplatform.

class Device {
    static var screen = Device()
    #if os(watchOS)
    var width: CGFloat = WKInterfaceDevice.current().screenBounds.size.width
    var height: CGFloat = WKInterfaceDevice.current().screenBounds.size.height
    #elseif os(iOS)
    var width: CGFloat = UIScreen.main.bounds.size.width
    var height: CGFloat = UIScreen.main.bounds.size.height
    #elseif os(macOS)
    // You could implement this to force a CGFloat and get the full device screen size width regardless of the window size with .frame.size.width
    var width: CGFloat? = NSScreen.main?.visibleFrame.size.width
    var height: CGFloat? = NSScreen.main?.visibleFrame.size.height
    #endif
}

Then you can use it like this:

let screenWidth = Device.screen.width
let screenHeight = Device.screen.height

1 Comment

Keep in mind that it is very rare to ever need to know the actual screen size.
3

I ended up having to create a wrapper that uses GeometryReader to get the size and THEN show my view since my view was bigger than the screen size and therefore broke the GeometryReader.

struct SpaceSized<Content: View>: View {
    @Binding var spaceSize: CGSize
    @State private var content: (CGSize) -> Content

    init(spaceSize: Binding<CGSize>, @ViewBuilder content: @escaping (CGSize) -> Content) {
        self._spaceSize = spaceSize
        self.content = content
    }

    var body: some View {
        if spaceSize != .zero {
            // Pass along size so that we can use within the block since it won't pull access
            content(spaceSize)
        } else {
            GeometryReader { proxy in
                // we just want the reader to get triggered, so let's use a color (won't ever be shown so irrelevant)
                Color.purple
                .onAppear {
                    spaceSize = proxy.size
                }
            }
            .ignoresSafeArea()
        }
    }
}

Then I can call it around the view that I will be using:

        SpaceSized(spaceSize: $screenSize) { ss in
            MyView(availableSpace: $screenSize)
        }

Hope this helps someone else.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.