1

I have a large amount of text, that I would like to display in a Text() view. If I wrap the view in a ScrollView I am able to scroll in either .horizontal, or .vertical direction.

However if I want scrolling in both directions, the layout is completely wrong. Almost as if there is a negative offset.

import SwiftUI

struct ContentView: View {
    let text = """
    <?xml version="1.0"?>
    <PrettyXML>
    <root xmlns="urn:schemas-upnp-org:device-1-0" xmlns:dlna="urn:schemas-dlna-org:device-1-0">
      <specVersion>
        <major>1</major>
        <minor>0</minor>
      </specVersion>
      <device>
        <deviceType>urn:schemas-upnp-org:device:MediaRenderer:2</deviceType>
        <friendlyName>Sample Renderer</friendlyName>
        <manufacturer></manufacturer>
        <manufacturerURL></manufacturerURL>
        <modelDescription/>
        <modelName>Test device</modelName>
        <modelNumber/>
        <modelURL></modelURL>
        <serialNumber>12354-12312789-123987129873</serialNumber>
        <UDN>uuid:12312311-1234-1234-1234-123456789011</UDN>
        <iconList>
          <icon>
            <mimetype>image/png</mimetype>
            <width>120</width>
            <height>120</height>
            <depth>24</depth>
            <url>/image-120x120x24.png</url>
          </icon>
          <icon>
            <mimetype>image/png</mimetype>
            <width>48</width>
            <height>48</height>
            <depth>24</depth>
            <url>/image-48x48x24.png</url>
          </icon>
          <icon>
            <mimetype>image/jpeg</mimetype>
            <width>120</width>
            <height>120</height>
            <depth>24</depth>
            <url>/image-120x120x24.jpg</url>
          </icon>
          <icon>
            <mimetype>image/jpeg</mimetype>
            <width>48</width>
            <height>48</height>
            <depth>24</depth>
            <url>/image-48x48x24.jpg</url>
          </icon>
        </iconList>
        <serviceList>
          <service>
            <serviceType>urn:schemas-upnp-org:service:ConnectionManager:2</serviceType>
            <serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
            <SCPDURL>/xml/ConnectionManager.xml</SCPDURL>
            <controlURL>/Control/ConnectionManager</controlURL>
            <eventSubURL>/Event/ConnectionManager</eventSubURL>
          </service>
          <service>
            <serviceType>urn:schemas-upnp-org:service:AVTransport:2</serviceType>
            <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
            <SCPDURL>/xml/AVTransport2.xml</SCPDURL>
            <controlURL>/Control/AVTransport</controlURL>
            <eventSubURL>/Event/AVTransport</eventSubURL>
          </service>
          <service>
            <serviceType>urn:schemas-upnp-org:service:RenderingControl:2</serviceType>
            <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
            <SCPDURL>/xml/RenderingControl2.xml</SCPDURL>
            <controlURL>/Control/Renderer/RygelRenderingControl</controlURL>
            <eventSubURL>/Event/Renderer/RygelRenderingControl</eventSubURL>
          </service>
        </serviceList>
        <presentationURL>http://10.0.0.1:80/</presentationURL>
        <dlna:X_DLNADOC>DMR-1.51</dlna:X_DLNADOC>
      </device>
    </root>
    """

    var body: some View {
//        ScrollView(.horizontal) {
//        ScrollView(.vertical) {
        ScrollView([.horizontal, .vertical]) {
            Text(text)
        }
    }
}

Is there a better approach to this, or is it just a bug that has no workaround currently? My only other thought would be to use a wrapped UITextView, which will only allow vertical scrolling, but if there is a way to manually set the frame width to be as large as necessary, then allow the ScrollView to only scroll horizontally.

An incredibly ugly way of solving this would be:

ScrollView(.horizontal, showsIndicators: false) {
  ScrollView(.vertical, showsIndicators: false) {
    Text(text)
  }
}

However this only allows scrolling in one direction at a time which just feels unnatural.

It seems like this behaviour is also tied to updating views. Consider the following example which simulates an redraw:

struct ContentView: View {
    @State var labelText = "Loading"

    let text = """
    <?xml version="1.0"?>
    <PrettyXML>
    <root xmlns="urn:schemas-upnp-org:device-1-0" xmlns:dlna="urn:schemas-dlna-org:device-1-0">
      <specVersion>
        <major>1</major>
        <minor>0</minor>
      </specVersion>
      <device>
        <deviceType>urn:schemas-upnp-org:device:MediaRenderer:2</deviceType>
        <friendlyName>Sample Renderer</friendlyName>
        <manufacturer></manufacturer>
        <manufacturerURL></manufacturerURL>
        <modelDescription/>
        <modelName>Test device</modelName>
        <modelNumber/>
        <modelURL></modelURL>
        <serialNumber>12354-12312789-123987129873</serialNumber>
        <UDN>uuid:12312311-1234-1234-1234-123456789011</UDN>
        <iconList>
          <icon>
            <mimetype>image/png</mimetype>
            <width>120</width>
            <height>120</height>
            <depth>24</depth>
            <url>/image-120x120x24.png</url>
          </icon>
          <icon>
            <mimetype>image/png</mimetype>
            <width>48</width>
            <height>48</height>
            <depth>24</depth>
            <url>/image-48x48x24.png</url>
          </icon>
          <icon>
            <mimetype>image/jpeg</mimetype>
            <width>120</width>
            <height>120</height>
            <depth>24</depth>
            <url>/image-120x120x24.jpg</url>
          </icon>
          <icon>
            <mimetype>image/jpeg</mimetype>
            <width>48</width>
            <height>48</height>
            <depth>24</depth>
            <url>/image-48x48x24.jpg</url>
          </icon>
        </iconList>
        <serviceList>
          <service>
            <serviceType>urn:schemas-upnp-org:service:ConnectionManager:2</serviceType>
            <serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
            <SCPDURL>/xml/ConnectionManager.xml</SCPDURL>
            <controlURL>/Control/ConnectionManager</controlURL>
            <eventSubURL>/Event/ConnectionManager</eventSubURL>
          </service>
          <service>
            <serviceType>urn:schemas-upnp-org:service:AVTransport:2</serviceType>
            <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
            <SCPDURL>/xml/AVTransport2.xml</SCPDURL>
            <controlURL>/Control/AVTransport</controlURL>
            <eventSubURL>/Event/AVTransport</eventSubURL>
          </service>
          <service>
            <serviceType>urn:schemas-upnp-org:service:RenderingControl:2</serviceType>
            <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
            <SCPDURL>/xml/RenderingControl2.xml</SCPDURL>
            <controlURL>/Control/Renderer/RygelRenderingControl</controlURL>
            <eventSubURL>/Event/Renderer/RygelRenderingControl</eventSubURL>
          </service>
        </serviceList>
        <presentationURL>http://10.0.0.1:80/</presentationURL>
        <dlna:X_DLNADOC>DMR-1.51</dlna:X_DLNADOC>
      </device>
    </root>
    """


    var body: some View {
         ScrollView([.horizontal, .vertical]) {
            Text(self.labelText)
         }
         .onAppear() {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
                self.labelText = self.text
            }
        }
    }
}

1 Answer 1

2

If you don't have a smart way, here is a stupid one. You may add conditions to adapt different size class.

struct ContentView: View {
  let text = "" /// Your long multiline text

  var body: some View {
    GeometryReader { (geometry: GeometryProxy) in
      ScrollView([.horizontal, .vertical]) {
        VStack {
          Spacer(minLength: geometry.size.height)
          HStack {
            Spacer(minLength: geometry.size.width)
            Text(self.text)
          }
        }
      }
    }
  }
}

A better way is to add a viewModifier, which make things automatic.

struct ContentView: View {
  let text = "" // Your multiline text 

  var body: some View {
    GeometryReader { p in
      ScrollView([.horizontal, .vertical]) {
        Text(self.text)
          .modifier(SimpleOffset(frameSize: p.size))
      }
    }
  }
}

struct SimpleOffset: GeometryEffect {
  var frameSize: CGSize
  func effectValue(size: CGSize) -> ProjectionTransform {
    ProjectionTransform(CGAffineTransform(translationX: (size.width - frameSize.width) / 2, y: (size.height - frameSize.height) / 2))
  }
}

Here is the solution with pure alignments.

 var body: some View {

     ScrollView([.horizontal, .vertical]) {
        Text(self.text).alignmentGuide(HorizontalAlignment.center) { (v) -> CGFloat in
            return (v[HorizontalAlignment.center] + v[.leading]) / 2
        }.alignmentGuide(VerticalAlignment.center) { (v) -> CGFloat in
            return (v[VerticalAlignment.center] + v[.top]) / 2
        }.frame(alignment: .center )
     }

}

The full code for the align method is like this :

            struct ContentView: View {


                let text = """
                <?xml version="1.0"?>
                <PrettyXML>
                <root xmlns="urn:schemas-upnp-org:device-1-0" xmlns:dlna="urn:schemas-dlna-org:device-1-0">
                  <specVersion>
                    <major>1</major>
                    <minor>0</minor>
                  </specVersion>
                  <device>
                    <deviceType>urn:schemas-upnp-org:device:MediaRenderer:2</deviceType>
                    <friendlyName>Sample Renderer</friendlyName>
                    <manufacturer></manufacturer>
                    <manufacturerURL></manufacturerURL>
                    <modelDescription/>
                    <modelName>Test device</modelName>
                    <modelNumber/>
                    <modelURL></modelURL>
                    <serialNumber>12354-12312789-123987129873</serialNumber>
                    <UDN>uuid:12312311-1234-1234-1234-123456789011</UDN>
                    <iconList>
                      <icon>
                        <mimetype>image/png</mimetype>
                        <width>120</width>
                        <height>120</height>
                        <depth>24</depth>
                        <url>/image-120x120x24.png</url>
                      </icon>
                      <icon>
                        <mimetype>image/png</mimetype>
                        <width>48</width>
                        <height>48</height>
                        <depth>24</depth>
                        <url>/image-48x48x24.png</url>
                      </icon>
                      <icon>
                        <mimetype>image/jpeg</mimetype>
                        <width>120</width>
                        <height>120</height>
                        <depth>24</depth>
                        <url>/image-120x120x24.jpg</url>
                      </icon>
                      <icon>
                        <mimetype>image/jpeg</mimetype>
                        <width>48</width>
                        <height>48</height>
                        <depth>24</depth>
                        <url>/image-48x48x24.jpg</url>
                      </icon>
                    </iconList>
                    <serviceList>
                      <service>
                        <serviceType>urn:schemas-upnp-org:service:ConnectionManager:2</serviceType>
                        <serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
                        <SCPDURL>/xml/ConnectionManager.xml</SCPDURL>
                        <controlURL>/Control/ConnectionManager</controlURL>
                        <eventSubURL>/Event/ConnectionManager</eventSubURL>
                      </service>
                      <service>
                        <serviceType>urn:schemas-upnp-org:service:AVTransport:2</serviceType>
                        <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
                        <SCPDURL>/xml/AVTransport2.xml</SCPDURL>
                        <controlURL>/Control/AVTransport</controlURL>
                        <eventSubURL>/Event/AVTransport</eventSubURL>
                      </service>
                      <service>
                        <serviceType>urn:schemas-upnp-org:service:RenderingControl:2</serviceType>
                        <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
                        <SCPDURL>/xml/RenderingControl2.xml</SCPDURL>
                        <controlURL>/Control/Renderer/RygelRenderingControl</controlURL>
                        <eventSubURL>/Event/Renderer/RygelRenderingControl</eventSubURL>
                      </service>
                    </serviceList>
                    <presentationURL>http://10.0.0.1:80/</presentationURL>
                    <dlna:X_DLNADOC>DMR-1.51</dlna:X_DLNADOC>
                  </device>
                </root>
                """


                var body: some View {

                     ScrollView([.horizontal, .vertical]) {
                        Text(self.text).alignmentGuide(HorizontalAlignment.center) { (v) -> CGFloat in
                            return (v[HorizontalAlignment.center] + v[.leading]) / 2
                        }.alignmentGuide(VerticalAlignment.center) { (v) -> CGFloat in
                            return (v[VerticalAlignment.center] + v[.top]) / 2
                        }.frame(alignment: .center )
                     }

                }

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

6 Comments

Unfortunately, that still leaves the scroll view offset and unusable.
The second view modifier method works as I'd expect. It could benefit from some top and bottom padding on iPhone X/XS. I'll mark as accepted, although I don't think we should have to do this for two-axis scrolling!
I added a solution with pure alignments and that's should be what you expect.
Sadly the alignment guides didn't work either. The label is still very offset so can't be viewed.
I don't see your point. Can you repeat what I see in above full codes.
|

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.