Skip to content

Commit 0c613a2

Browse files
authored
Merge pull request #7 from GoodRequest/feature/swiftui-readable-content-guide
feat: SwiftUI - readable content guideline modifier
2 parents dafb9e8 + 60fbdb6 commit 0c613a2

File tree

1 file changed

+199
-0
lines changed

1 file changed

+199
-0
lines changed
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
//
2+
// ReadableContentWidthModifier.swift
3+
//
4+
// GoodSwiftUI
5+
// Created by Filip Šašala on 31/12/2023.
6+
//
7+
8+
import SwiftUI
9+
10+
// MARK: - Readable content width view
11+
12+
public struct FittingReadableWidth<Content: View>: View {
13+
14+
private let alignment: Alignment
15+
private let content: () -> Content
16+
17+
public init(alignment: Alignment = .center, content: @escaping () -> Content) {
18+
self.alignment = alignment
19+
self.content = content
20+
}
21+
22+
public var body: some View {
23+
content().fittingReadableWidth(alignment: alignment)
24+
}
25+
26+
}
27+
28+
// MARK: - Readable content width modifier
29+
30+
private struct ReadableContentWidthModifier: ViewModifier {
31+
32+
let alignment: Alignment
33+
34+
func body(content: Content) -> some View {
35+
content
36+
.modifier(ReadableContentWidthPaddingModifier(alignment: alignment))
37+
.modifier(ReadableContentWidthMeasurementModifier())
38+
}
39+
40+
}
41+
42+
public extension View {
43+
44+
func fittingReadableWidth(alignment: Alignment = .center) -> some View {
45+
modifier(ReadableContentWidthModifier(alignment: alignment))
46+
}
47+
48+
}
49+
50+
// MARK: - Environment
51+
52+
private extension EnvironmentValues {
53+
54+
@Entry var readableContentInsets = EdgeInsets()
55+
56+
}
57+
58+
// MARK: - Padding modifier
59+
60+
private struct ReadableContentWidthPaddingModifier: ViewModifier {
61+
62+
@Environment(\.readableContentInsets) private var readableContentInsets
63+
64+
let alignment: Alignment
65+
66+
func body(content: Content) -> some View {
67+
content
68+
.frame(maxWidth: .infinity, alignment: alignment)
69+
.padding(.leading, readableContentInsets.leading)
70+
.padding(.trailing, readableContentInsets.trailing)
71+
}
72+
73+
}
74+
75+
// MARK: - Measurement modifier
76+
77+
private struct ReadableContentWidthMeasurementModifier: ViewModifier {
78+
79+
@State private var readableContentInsets = EdgeInsets()
80+
81+
func body(content: Content) -> some View {
82+
content
83+
.environment(\.readableContentInsets, readableContentInsets)
84+
.background(LayoutGuides(onChangeOfReadableContentInsets: {
85+
readableContentInsets = $0
86+
}))
87+
}
88+
89+
}
90+
91+
// MARK: - UIKit LayoutGuides view
92+
93+
private struct LayoutGuides: UIViewRepresentable {
94+
95+
let onChangeOfReadableContentInsets: (EdgeInsets) -> ()
96+
97+
func makeUIView(context: Context) -> LayoutGuidesView {
98+
let uiView = LayoutGuidesView()
99+
uiView.onChangeOfReadableContentInsets = self.onChangeOfReadableContentInsets
100+
return uiView
101+
}
102+
103+
func updateUIView(_ uiView: LayoutGuidesView, context: Context) {
104+
uiView.onChangeOfReadableContentInsets = self.onChangeOfReadableContentInsets
105+
}
106+
107+
}
108+
109+
private final class LayoutGuidesView: UIView {
110+
111+
fileprivate var onChangeOfReadableContentInsets: (EdgeInsets) -> () = { _ in }
112+
private var previousReadableContentInsets: EdgeInsets?
113+
114+
override func layoutMarginsDidChange() {
115+
super.layoutMarginsDidChange()
116+
updateReadableContent()
117+
}
118+
119+
override func layoutSubviews() {
120+
super.layoutSubviews()
121+
updateReadableContent()
122+
}
123+
124+
override var frame: CGRect {
125+
didSet { updateReadableContent() }
126+
}
127+
128+
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
129+
super.traitCollectionDidChange(previousTraitCollection)
130+
131+
if traitCollection.layoutDirection != previousTraitCollection?.layoutDirection {
132+
updateReadableContent()
133+
}
134+
}
135+
136+
}
137+
138+
private extension LayoutGuidesView {
139+
140+
func updateReadableContent() {
141+
let isRTLLanguage = traitCollection.layoutDirection == .rightToLeft
142+
let readableLayoutFrame = readableContentGuide.layoutFrame
143+
144+
let readableEdgeInsets = UIEdgeInsets(
145+
top: readableLayoutFrame.minY - bounds.minY,
146+
left: readableLayoutFrame.minX - bounds.minX,
147+
bottom: -(readableLayoutFrame.maxY - bounds.maxY),
148+
right: -(readableLayoutFrame.maxX - bounds.maxX)
149+
)
150+
let readableContentInsets = EdgeInsets(
151+
top: readableEdgeInsets.top,
152+
leading: isRTLLanguage ? readableEdgeInsets.right : readableEdgeInsets.left,
153+
bottom: readableEdgeInsets.bottom,
154+
trailing: isRTLLanguage ? readableEdgeInsets.left : readableEdgeInsets.right
155+
)
156+
157+
guard previousReadableContentInsets != readableContentInsets else { return }
158+
defer { previousReadableContentInsets = readableContentInsets }
159+
160+
self.onChangeOfReadableContentInsets(readableContentInsets)
161+
}
162+
163+
}
164+
165+
// MARK: - Previews
166+
167+
@available(iOS 17.0, *)
168+
#Preview {
169+
FittingReadableWidth {
170+
Rectangle()
171+
.foregroundStyle(.cyan)
172+
}
173+
}
174+
175+
private final class PreviewViewController: UIViewController {
176+
177+
override func viewDidLoad() {
178+
super.viewDidLoad()
179+
180+
let rectangle = UIView()
181+
rectangle.translatesAutoresizingMaskIntoConstraints = false
182+
rectangle.backgroundColor = .systemRed
183+
184+
view.addSubview(rectangle)
185+
186+
NSLayoutConstraint.activate([
187+
rectangle.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
188+
rectangle.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
189+
rectangle.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
190+
rectangle.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
191+
])
192+
}
193+
194+
}
195+
196+
@available(iOS 17.0, *)
197+
#Preview {
198+
PreviewViewController()
199+
}

0 commit comments

Comments
 (0)