Skip to content

Commit

Permalink
Added layout guides.
Browse files Browse the repository at this point in the history
  • Loading branch information
ivanvorobei committed Nov 26, 2024
1 parent cfbc3b0 commit 7dee82a
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 0 deletions.
119 changes: 119 additions & 0 deletions Sources/SwiftUIExtension/LayoutGuides/LayoutGuides.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import SwiftUI

extension View {

public func fitToReadableContentWidth(alignment: Alignment = .center) -> some View {
self.modifier(FitLayoutGuidesWidth(alignment: alignment, kind: .readableContent))
}

public func fitToLayoutMarginsWidth(alignment: Alignment = .center) -> some View {
self.modifier(FitLayoutGuidesWidth(alignment: alignment, kind: .layoutMargins))
}

public func measureLayoutGuides() -> some View {
self.modifier(LayoutGuidesModifier())
}
}

public struct WithLayoutMargins<Content>: View where Content: View {

let content: (EdgeInsets) -> Content

public init(@ViewBuilder content: @escaping (EdgeInsets) -> Content) {
self.content = content
}

public init(@ViewBuilder content: @escaping () -> Content) {
self.content = { _ in content() }
}

public var body: some View {
InsetContent(content: content)
.measureLayoutGuides()
}

private struct InsetContent: View {

let content: (EdgeInsets) -> Content

@Environment(\.layoutMarginsInsets) var layoutMarginsInsets

var body: some View {
content(layoutMarginsInsets)
}
}
}

// MARK: - Private

internal struct FitLayoutGuidesWidth: ViewModifier {

enum Kind {
case layoutMargins
case readableContent
}

let alignment: Alignment
let kind: Kind

func body(content: Content) -> some View {
switch kind {
case .layoutMargins:
content.modifier(InsetLayoutMargins(alignment: alignment))
.measureLayoutGuides()
case .readableContent:
content.modifier(InsetReadableContent(alignment: alignment))
.measureLayoutGuides()
}
}

private struct InsetReadableContent: ViewModifier {

let alignment: Alignment
@Environment(\.readableContentInsets) var readableContentInsets

func body(content: Content) -> some View {
content
.frame(maxWidth: .infinity, alignment: alignment)
.padding(.leading, readableContentInsets.leading)
.padding(.trailing, readableContentInsets.trailing)
}
}

private struct InsetLayoutMargins: ViewModifier {

let alignment: Alignment
@Environment(\.layoutMarginsInsets) var layoutMarginsInsets

func body(content: Content) -> some View {
content
.frame(maxWidth: .infinity, alignment: alignment)
.padding(.leading, layoutMarginsInsets.leading)
.padding(.trailing, layoutMarginsInsets.trailing)
}
}
}

internal struct LayoutGuidesModifier: ViewModifier {

@State var layoutMarginsInsets: EdgeInsets = .init()
@State var readableContentInsets: EdgeInsets = .init()

func body(content: Content) -> some View {
content
#if os(iOS) || os(tvOS)
.environment(\.layoutMarginsInsets, layoutMarginsInsets)
.environment(\.readableContentInsets, readableContentInsets)
.background(
LayoutGuidesObserverView(
onLayoutMarginsGuideChange: {
layoutMarginsInsets = $0
},
onReadableContentGuideChange: {
readableContentInsets = $0
})
)
#endif
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import SwiftUI

private struct LayoutMarginsGuidesKey: EnvironmentKey {
static var defaultValue: EdgeInsets { .init() }
}

private struct ReadableContentGuidesKey: EnvironmentKey {
static var defaultValue: EdgeInsets { .init() }
}

extension EnvironmentValues {

public var layoutMarginsInsets: EdgeInsets {
get { self[LayoutMarginsGuidesKey.self] }
set { self[LayoutMarginsGuidesKey.self] = newValue }
}

public var readableContentInsets: EdgeInsets {
get { self[ReadableContentGuidesKey.self] }
set { self[ReadableContentGuidesKey.self] = newValue }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#if os(iOS) || os(tvOS)
import UIKit
import SwiftUI

struct LayoutGuidesObserverView: UIViewRepresentable {

let onLayoutMarginsGuideChange: (EdgeInsets) -> Void
let onReadableContentGuideChange: (EdgeInsets) -> Void

func makeUIView(context: Context) -> LayoutGuidesView {
let uiView = LayoutGuidesView()
uiView.onLayoutMarginsGuideChange = onLayoutMarginsGuideChange
uiView.onReadableContentGuideChange = onReadableContentGuideChange
return uiView
}

func updateUIView(_ uiView: LayoutGuidesView, context: Context) {
uiView.onLayoutMarginsGuideChange = onLayoutMarginsGuideChange
uiView.onReadableContentGuideChange = onReadableContentGuideChange
}

final class LayoutGuidesView: UIView {
var onLayoutMarginsGuideChange: (EdgeInsets) -> Void = { _ in }
var onReadableContentGuideChange: (EdgeInsets) -> Void = { _ in }

override func layoutMarginsDidChange() {
super.layoutMarginsDidChange()
updateLayoutMargins()
updateReadableContent()
}

override func layoutSubviews() {
super.layoutSubviews()
updateReadableContent()
}

// `layoutSubviews` doesn't seem late enough to retrieve an up-to-date `readableContentGuide`
// in some cases, like when toggling the sidebar in a NavigationSplitView on iPad.
// It seems that observing the `frame` is enough to fix this edge case, but a better
// heuristic would be preferable.
override var frame: CGRect {
didSet {
self.updateReadableContent()
}
}

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.layoutDirection != previousTraitCollection?.layoutDirection {
updateReadableContent()
}
}

var previousLayoutMargins: EdgeInsets? = nil
func updateLayoutMargins() {
let edgeInsets = EdgeInsets(
top: directionalLayoutMargins.top,
leading: directionalLayoutMargins.leading,
bottom: directionalLayoutMargins.bottom,
trailing: directionalLayoutMargins.trailing
)
guard previousLayoutMargins != edgeInsets else { return }
onLayoutMarginsGuideChange(edgeInsets)
previousLayoutMargins = edgeInsets
}

var previousReadableContentGuide: EdgeInsets? = nil
func updateReadableContent() {
let isRightToLeft = traitCollection.layoutDirection == .rightToLeft
let layoutFrame = readableContentGuide.layoutFrame

let readableContentInsets =
UIEdgeInsets(
top: layoutFrame.minY - bounds.minY,
left: layoutFrame.minX - bounds.minX,
bottom: -(layoutFrame.maxY - bounds.maxY),
right: -(layoutFrame.maxX - bounds.maxX)
)
let edgeInsets = EdgeInsets(
top: readableContentInsets.top,
leading: isRightToLeft ? readableContentInsets.right : readableContentInsets.left,
bottom: readableContentInsets.bottom,
trailing: isRightToLeft ? readableContentInsets.left : readableContentInsets.right
)
guard previousReadableContentGuide != edgeInsets else { return }
onReadableContentGuideChange(edgeInsets)
previousReadableContentGuide = edgeInsets
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,23 @@ extension View {

return self.modifier(sheet)
}

public func modalSheet<Content>(
isPresented: Binding<Bool>,
dismissable: Bool,
onDismiss: (() -> Void)? = nil,
@ViewBuilder content: @escaping () -> Content
) -> some View where Content : View {

let sheet = ModalSheetModifier(
isPresented: isPresented,
selection: .constant(""),
dismissable: dismissable,
onDismiss: onDismiss,
modalContent: content
)

return self.modifier(sheet)
}
}
#endif

0 comments on commit 7dee82a

Please sign in to comment.