Reputation: 1997
I'm having difficulties aligning the navigation title with my content view in SwiftUI. This operation is trivial to do in UIKit especially using collection view flow layout.
n.b. This is for an experimental project and I'm using iOS 15.
I've tried many different approaches :
This gives us the right alignment between the navigation title, the header and cells on the iPhone 12 Max but not on the iPhone 12
The scroll view doesn't extend over the edges as it would in the Apple Music app.
Is there a way to fix the scroll view?
var body: some View {
NavigationView {
GeometryReader { proxy in
let cellWidth = proxy.size.width * 0.4
List {
ForEach(0...10, id: \.self) { rowIndex in
Section(header: Text("Unknown").font(.title2).redacted(reason: .placeholder)) {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(alignment: .top, spacing: 16) {
ForEach(0...10, id: \.self) { index in
VStack(alignment: .leading, spacing: 2) {
RoundedRectangle(cornerRadius: 8)
.foregroundColor(.red)
.frame(width: cellWidth, height: cellWidth)
VStack(alignment: .leading, spacing: 2) {
Text("Example")
.foregroundColor(.primary)
.font(.subheadline)
.lineLimit(1)
.redacted(reason: .placeholder)
Text("Example")
.foregroundColor(.secondary)
.font(.subheadline)
.lineLimit(1)
.redacted(reason: .placeholder)
}
}
}
}
}
}
}
}
.listStyle(InsetListStyle())
.navigationBarTitle("Experimental")
}
}
}
VStack
and first cell with the title. For example on an iPhone 12 Pro Max, a padding of 20 gives the right alignment and on an iPhone 12 we will be off by 2 pixels or so using the same padding. var bodyUsingVStack: some View {
NavigationView {
GeometryReader { proxy in
let cellWidth = proxy.size.width * 0.4
ScrollView {
VStack(alignment: .leading, spacing: 16) {
ForEach(0...10, id: \.self) { rowIndex in
Section(
header: Text("Unknown")
.font(.title2)
.padding(.leading, 20)
// .redacted(reason: .placeholder)
) {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(alignment: .top, spacing: 16) {
ForEach(0...10, id: \.self) { index in
VStack(alignment: .leading, spacing: 2) {
RoundedRectangle(cornerRadius: 8)
.foregroundColor(.red)
.frame(width: cellWidth, height: cellWidth)
VStack(alignment: .leading, spacing: 2) {
Text("Example")
.foregroundColor(.primary)
.font(.subheadline)
.lineLimit(1)
.redacted(reason: .placeholder)
Text("Example")
.foregroundColor(.secondary)
.font(.subheadline)
.lineLimit(1)
.redacted(reason: .placeholder)
}
}
.padding(.leading, index == 0 ? 22 : 0)
}
}
}
}
}
}
}
.listStyle(InsetListStyle())
.navigationBarTitle("Experimental")
}
}
}
There must be a simpler way and it is crazy that SwiftUI allows so much flexibility and yet we often get stuck for hours or days trying to do trivial things.
Upvotes: 3
Views: 1107
Reputation: 1997
Measuring the padding on all iPhone models (and iPad) I realised that if the screen width is less than 414, padding is 16 and 24 otherwise.
import SwiftUI
public func margin(for width: Double) -> Double {
guard !width.isZero else { return 0 }
return width >= 414 ? 20 : 16
}
Upvotes: 1
Reputation: 1997
This is the simplest solution I found in which everything is aligned regardless of the device (not tested on iPad yet).
It uses Introspect to capture the frame of the large title view's label.
.introspectNavigationController {
let padding = $0.navigationBar
.subviews.first { String(describing: type(of: $0)).hasSuffix("LargeTitleView") }?
.subviews.first { $0 is UILabel }?
.frame.origin.x ?? 0
if !padding.isZero, self.padding != padding {
self.padding = padding
}
}
@State private var padding: CGFloat = 0
var body: some View {
NavigationView {
GeometryReader { proxy in
let cellWidth = proxy.size.width * 0.4
ScrollView {
VStack(alignment: .leading, spacing: 16) {
ForEach(0...10, id: \.self) { rowIndex in
Section(
header: Text("Unknown")
.font(.title2)
.padding(.leading, padding)
.redacted(reason: .placeholder)
) {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(alignment: .top, spacing: 16) {
ForEach(0...10, id: \.self) { index in
VStack(alignment: .leading, spacing: 2) {
RoundedRectangle(cornerRadius: 8)
.foregroundColor(.red)
.frame(width: cellWidth, height: cellWidth)
VStack(alignment: .leading, spacing: 2) {
Text("Example")
.foregroundColor(.primary)
.font(.subheadline)
.lineLimit(1)
.redacted(reason: .placeholder)
Text("Example")
.foregroundColor(.secondary)
.font(.subheadline)
.lineLimit(1)
.redacted(reason: .placeholder)
}
}
.padding(.leading, index == 0 ? padding : 0)
}
}
}
}
}
}
}
.listStyle(InsetListStyle())
.navigationBarTitle("Experimental")
.introspectNavigationController {
let padding = $0.navigationBar
.subviews.first { String(describing: type(of: $0)).hasSuffix("LargeTitleView") }?
.subviews.first { $0 is UILabel }?
.frame.origin.x ?? 0
if !padding.isZero, self.padding != padding {
self.padding = padding
}
}
}
}
}
Upvotes: 1
Reputation: 30341
I would do it by reading the padding, then using that to extend the width of the ScrollView
.
Example below. Changes:
Cells
is a separate view because otherwise it couldn't compile in reasonable time (for me). Would change from machine-to-machine.background
containing the GeometryReader
.ScrollView
.struct ContentView: View {
@State private var padding: CGFloat?
var body: some View {
NavigationView {
GeometryReader { proxy in
let cellWidth = proxy.size.width * 0.4
List {
ForEach(0...10, id: \.self) { rowIndex in
Section(header: Text("Unknown").font(.title2).redacted(reason: .placeholder)) {
ScrollView(.horizontal, showsIndicators: false) {
Cells(cellWidth: cellWidth)
.background {
if padding == nil {
GeometryReader { geo in
Color.clear.onAppear {
padding = geo.frame(in: .global).minX
}
}
}
}
.padding(.horizontal, padding ?? 0)
}
.padding(.horizontal, -(padding ?? 0))
}
}
}
.listStyle(InsetListStyle())
.navigationBarTitle("Experimental")
}
}
.navigationViewStyle(.stack)
}
}
struct Cells: View {
let cellWidth: CGFloat
var body: some View {
LazyHStack(alignment: .top, spacing: 16) {
ForEach(0...10, id: \.self) { index in
VStack(alignment: .leading, spacing: 2) {
RoundedRectangle(cornerRadius: 8)
.foregroundColor(.red)
.frame(width: cellWidth, height: cellWidth)
VStack(alignment: .leading, spacing: 2) {
Text("Example")
.foregroundColor(.primary)
.font(.subheadline)
.lineLimit(1)
.redacted(reason: .placeholder)
Text("Example")
.foregroundColor(.secondary)
.font(.subheadline)
.lineLimit(1)
.redacted(reason: .placeholder)
}
}
}
}
}
}
Result:
Upvotes: -1