Skip to content

Commit 7dee82a

Browse files
committed
Added layout guides.
1 parent cfbc3b0 commit 7dee82a

File tree

4 files changed

+250
-0
lines changed

4 files changed

+250
-0
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import SwiftUI
2+
3+
extension View {
4+
5+
public func fitToReadableContentWidth(alignment: Alignment = .center) -> some View {
6+
self.modifier(FitLayoutGuidesWidth(alignment: alignment, kind: .readableContent))
7+
}
8+
9+
public func fitToLayoutMarginsWidth(alignment: Alignment = .center) -> some View {
10+
self.modifier(FitLayoutGuidesWidth(alignment: alignment, kind: .layoutMargins))
11+
}
12+
13+
public func measureLayoutGuides() -> some View {
14+
self.modifier(LayoutGuidesModifier())
15+
}
16+
}
17+
18+
public struct WithLayoutMargins<Content>: View where Content: View {
19+
20+
let content: (EdgeInsets) -> Content
21+
22+
public init(@ViewBuilder content: @escaping (EdgeInsets) -> Content) {
23+
self.content = content
24+
}
25+
26+
public init(@ViewBuilder content: @escaping () -> Content) {
27+
self.content = { _ in content() }
28+
}
29+
30+
public var body: some View {
31+
InsetContent(content: content)
32+
.measureLayoutGuides()
33+
}
34+
35+
private struct InsetContent: View {
36+
37+
let content: (EdgeInsets) -> Content
38+
39+
@Environment(\.layoutMarginsInsets) var layoutMarginsInsets
40+
41+
var body: some View {
42+
content(layoutMarginsInsets)
43+
}
44+
}
45+
}
46+
47+
// MARK: - Private
48+
49+
internal struct FitLayoutGuidesWidth: ViewModifier {
50+
51+
enum Kind {
52+
case layoutMargins
53+
case readableContent
54+
}
55+
56+
let alignment: Alignment
57+
let kind: Kind
58+
59+
func body(content: Content) -> some View {
60+
switch kind {
61+
case .layoutMargins:
62+
content.modifier(InsetLayoutMargins(alignment: alignment))
63+
.measureLayoutGuides()
64+
case .readableContent:
65+
content.modifier(InsetReadableContent(alignment: alignment))
66+
.measureLayoutGuides()
67+
}
68+
}
69+
70+
private struct InsetReadableContent: ViewModifier {
71+
72+
let alignment: Alignment
73+
@Environment(\.readableContentInsets) var readableContentInsets
74+
75+
func body(content: Content) -> some View {
76+
content
77+
.frame(maxWidth: .infinity, alignment: alignment)
78+
.padding(.leading, readableContentInsets.leading)
79+
.padding(.trailing, readableContentInsets.trailing)
80+
}
81+
}
82+
83+
private struct InsetLayoutMargins: ViewModifier {
84+
85+
let alignment: Alignment
86+
@Environment(\.layoutMarginsInsets) var layoutMarginsInsets
87+
88+
func body(content: Content) -> some View {
89+
content
90+
.frame(maxWidth: .infinity, alignment: alignment)
91+
.padding(.leading, layoutMarginsInsets.leading)
92+
.padding(.trailing, layoutMarginsInsets.trailing)
93+
}
94+
}
95+
}
96+
97+
internal struct LayoutGuidesModifier: ViewModifier {
98+
99+
@State var layoutMarginsInsets: EdgeInsets = .init()
100+
@State var readableContentInsets: EdgeInsets = .init()
101+
102+
func body(content: Content) -> some View {
103+
content
104+
#if os(iOS) || os(tvOS)
105+
.environment(\.layoutMarginsInsets, layoutMarginsInsets)
106+
.environment(\.readableContentInsets, readableContentInsets)
107+
.background(
108+
LayoutGuidesObserverView(
109+
onLayoutMarginsGuideChange: {
110+
layoutMarginsInsets = $0
111+
},
112+
onReadableContentGuideChange: {
113+
readableContentInsets = $0
114+
})
115+
)
116+
#endif
117+
}
118+
}
119+
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import SwiftUI
2+
3+
private struct LayoutMarginsGuidesKey: EnvironmentKey {
4+
static var defaultValue: EdgeInsets { .init() }
5+
}
6+
7+
private struct ReadableContentGuidesKey: EnvironmentKey {
8+
static var defaultValue: EdgeInsets { .init() }
9+
}
10+
11+
extension EnvironmentValues {
12+
13+
public var layoutMarginsInsets: EdgeInsets {
14+
get { self[LayoutMarginsGuidesKey.self] }
15+
set { self[LayoutMarginsGuidesKey.self] = newValue }
16+
}
17+
18+
public var readableContentInsets: EdgeInsets {
19+
get { self[ReadableContentGuidesKey.self] }
20+
set { self[ReadableContentGuidesKey.self] = newValue }
21+
}
22+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#if os(iOS) || os(tvOS)
2+
import UIKit
3+
import SwiftUI
4+
5+
struct LayoutGuidesObserverView: UIViewRepresentable {
6+
7+
let onLayoutMarginsGuideChange: (EdgeInsets) -> Void
8+
let onReadableContentGuideChange: (EdgeInsets) -> Void
9+
10+
func makeUIView(context: Context) -> LayoutGuidesView {
11+
let uiView = LayoutGuidesView()
12+
uiView.onLayoutMarginsGuideChange = onLayoutMarginsGuideChange
13+
uiView.onReadableContentGuideChange = onReadableContentGuideChange
14+
return uiView
15+
}
16+
17+
func updateUIView(_ uiView: LayoutGuidesView, context: Context) {
18+
uiView.onLayoutMarginsGuideChange = onLayoutMarginsGuideChange
19+
uiView.onReadableContentGuideChange = onReadableContentGuideChange
20+
}
21+
22+
final class LayoutGuidesView: UIView {
23+
var onLayoutMarginsGuideChange: (EdgeInsets) -> Void = { _ in }
24+
var onReadableContentGuideChange: (EdgeInsets) -> Void = { _ in }
25+
26+
override func layoutMarginsDidChange() {
27+
super.layoutMarginsDidChange()
28+
updateLayoutMargins()
29+
updateReadableContent()
30+
}
31+
32+
override func layoutSubviews() {
33+
super.layoutSubviews()
34+
updateReadableContent()
35+
}
36+
37+
// `layoutSubviews` doesn't seem late enough to retrieve an up-to-date `readableContentGuide`
38+
// in some cases, like when toggling the sidebar in a NavigationSplitView on iPad.
39+
// It seems that observing the `frame` is enough to fix this edge case, but a better
40+
// heuristic would be preferable.
41+
override var frame: CGRect {
42+
didSet {
43+
self.updateReadableContent()
44+
}
45+
}
46+
47+
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
48+
super.traitCollectionDidChange(previousTraitCollection)
49+
if traitCollection.layoutDirection != previousTraitCollection?.layoutDirection {
50+
updateReadableContent()
51+
}
52+
}
53+
54+
var previousLayoutMargins: EdgeInsets? = nil
55+
func updateLayoutMargins() {
56+
let edgeInsets = EdgeInsets(
57+
top: directionalLayoutMargins.top,
58+
leading: directionalLayoutMargins.leading,
59+
bottom: directionalLayoutMargins.bottom,
60+
trailing: directionalLayoutMargins.trailing
61+
)
62+
guard previousLayoutMargins != edgeInsets else { return }
63+
onLayoutMarginsGuideChange(edgeInsets)
64+
previousLayoutMargins = edgeInsets
65+
}
66+
67+
var previousReadableContentGuide: EdgeInsets? = nil
68+
func updateReadableContent() {
69+
let isRightToLeft = traitCollection.layoutDirection == .rightToLeft
70+
let layoutFrame = readableContentGuide.layoutFrame
71+
72+
let readableContentInsets =
73+
UIEdgeInsets(
74+
top: layoutFrame.minY - bounds.minY,
75+
left: layoutFrame.minX - bounds.minX,
76+
bottom: -(layoutFrame.maxY - bounds.maxY),
77+
right: -(layoutFrame.maxX - bounds.maxX)
78+
)
79+
let edgeInsets = EdgeInsets(
80+
top: readableContentInsets.top,
81+
leading: isRightToLeft ? readableContentInsets.right : readableContentInsets.left,
82+
bottom: readableContentInsets.bottom,
83+
trailing: isRightToLeft ? readableContentInsets.left : readableContentInsets.right
84+
)
85+
guard previousReadableContentGuide != edgeInsets else { return }
86+
onReadableContentGuideChange(edgeInsets)
87+
previousReadableContentGuide = edgeInsets
88+
}
89+
}
90+
}
91+
#endif

Sources/SwiftUIExtension/Views/Mimicrate/ModalSheet/ModalSheet.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,23 @@ extension View {
2121

2222
return self.modifier(sheet)
2323
}
24+
25+
public func modalSheet<Content>(
26+
isPresented: Binding<Bool>,
27+
dismissable: Bool,
28+
onDismiss: (() -> Void)? = nil,
29+
@ViewBuilder content: @escaping () -> Content
30+
) -> some View where Content : View {
31+
32+
let sheet = ModalSheetModifier(
33+
isPresented: isPresented,
34+
selection: .constant(""),
35+
dismissable: dismissable,
36+
onDismiss: onDismiss,
37+
modalContent: content
38+
)
39+
40+
return self.modifier(sheet)
41+
}
2442
}
2543
#endif

0 commit comments

Comments
 (0)