Skip to content

Commit 32acaaa

Browse files
committed
Added modal sheet view and fixed existing objects.
1 parent 37530ca commit 32acaaa

File tree

8 files changed

+324
-3
lines changed

8 files changed

+324
-3
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import SwiftUI
2+
3+
extension View {
4+
5+
public func transitionBlurReplaceCombability() -> some View {
6+
modifier(TransitionBlurReplace())
7+
}
8+
}
9+
10+
struct TransitionBlurReplace: ViewModifier {
11+
12+
func body(content: Content) -> some View {
13+
if #available(iOS 18.0, *) {
14+
content
15+
.transition(.blurReplace.combined(with: .opacity))
16+
} else {
17+
content
18+
.transition(.opacity)
19+
}
20+
}
21+
}

Sources/SwiftUIExtension/Extensions/ImageExtension.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import SwiftUI
44
extension Image {
55

66
public static func load(url: URL, completion: @escaping (Image?) -> Void) {
7-
DispatchQueue.global(qos: .background).async {
7+
DispatchQueue.global(qos: .default).async {
88
if let data = try? Data(contentsOf: url) {
99

1010
#if canImport(UIKit)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import SwiftUI
2+
3+
extension ButtonStyle where Self == LargeButtonStyle {
4+
5+
public static func large(_ colorise: ButtonColorise) -> Self {
6+
return LargeButtonStyle(colorise)
7+
}
8+
}
9+
10+
public struct LargeButtonStyle: ButtonStyle {
11+
12+
@Environment(\.isEnabled) private var isEnabled: Bool
13+
14+
let colorise: ButtonColorise
15+
16+
init(_ colorise: ButtonColorise) {
17+
self.colorise = colorise
18+
}
19+
20+
public func makeBody(configuration: Configuration) -> some View {
21+
configuration.label
22+
.font(.headline)
23+
.foregroundColor(colorise.foregroundColor)
24+
.frame(maxWidth: .infinity)
25+
.padding(.vertical, 15)
26+
.padding(.horizontal, 10)
27+
.background {
28+
colorise.backgroundColor
29+
.clipShape(.rect(cornerRadius: 12))
30+
}
31+
.opacity(configuration.isPressed ? 0.7 : 1)
32+
.animation(.default, value: configuration.isPressed)
33+
.opacity(isEnabled ? 1 : 0.4)
34+
.animation(.default, value: isEnabled)
35+
}
36+
}
37+
38+
public enum ButtonColorise {
39+
40+
case colorful
41+
case tinted
42+
case grayed
43+
44+
var foregroundColor: Color {
45+
switch self {
46+
case .colorful:
47+
return .white
48+
case .tinted:
49+
return .accentColor
50+
case .grayed:
51+
return .accentColor
52+
}
53+
}
54+
var backgroundColor: Color {
55+
switch self {
56+
case .colorful:
57+
return .accentColor
58+
case .tinted:
59+
return .accentColor.opacity(0.12)
60+
case .grayed:
61+
return .gray.opacity(0.15)
62+
}
63+
}
64+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import SwiftUI
2+
3+
struct MimicrateCloseButton: View {
4+
5+
let action: () -> Void
6+
7+
var body: some View {
8+
Button {
9+
action()
10+
} label: {
11+
Image(systemName: "xmark")
12+
.font(.footnote.bold())
13+
.foregroundColor(.gray)
14+
.padding(7)
15+
.background {
16+
Circle()
17+
}
18+
.tint(.gray.opacity(0.2))
19+
}
20+
}
21+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import SwiftUI
2+
3+
extension View {
4+
5+
public func modalSheet<Content, Selection: Hashable>(
6+
isPresented: Binding<Bool>,
7+
selection: Binding<Selection>,
8+
dismissable: Bool,
9+
onDismiss: (() -> Void)? = nil,
10+
@ViewBuilder content: @escaping () -> Content
11+
) -> some View where Content : View {
12+
13+
let sheet = ModalSheetModifier(
14+
isPresented: isPresented,
15+
selection: selection,
16+
dismissable: dismissable,
17+
onDismiss: onDismiss,
18+
modalContent: content
19+
)
20+
21+
return self.modifier(sheet)
22+
}
23+
}
24+
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import SwiftUI
2+
3+
/**
4+
Idea have to reusable basic style of modal sheet content with title+descrition & action button.
5+
*/
6+
public struct ModalSheetContentView<Content: View, ActionContent: View>: View {
7+
8+
let title: String
9+
let description: String?
10+
let content: () -> Content
11+
let actionContent: () -> ActionContent
12+
let actionEnabled: Bool
13+
let action: () -> Void
14+
let dismissable: Bool
15+
16+
public init(
17+
title: String,
18+
description: String?,
19+
@ViewBuilder content: @escaping () -> Content,
20+
@ViewBuilder actionContent: @escaping () -> ActionContent,
21+
actionEnabled: Bool,
22+
action: @escaping () -> Void,
23+
dismissable: Bool
24+
) {
25+
self.title = title
26+
self.description = description
27+
self.content = content
28+
self.actionContent = actionContent
29+
self.actionEnabled = actionEnabled
30+
self.action = action
31+
32+
self.dismissable = dismissable
33+
}
34+
35+
public var body: some View {
36+
VStack(alignment: .center, spacing: .zero) {
37+
38+
VStack(alignment: .center) {
39+
Text(title)
40+
.font(.title.weight(.bold))
41+
42+
if let description {
43+
Text(description)
44+
}
45+
}
46+
47+
FixedSpacer(height: Spaces.default_double)
48+
content()
49+
FixedSpacer(height: Spaces.default_double)
50+
51+
Button {
52+
action()
53+
} label: {
54+
actionContent()
55+
}
56+
.buttonStyle(.large(.tinted))
57+
.disabled(!actionEnabled)
58+
}
59+
.multilineTextAlignment(.center)
60+
}
61+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import SwiftUI
2+
import SwiftBoost
3+
4+
struct ModalSheetModifier<ModalContent: View, Selection: Hashable>: ViewModifier {
5+
6+
// States
7+
@Binding private var isPresented: Bool
8+
@Binding private var selection: Selection
9+
private let dismissable: Bool
10+
private let onDismiss: (() -> Void)?
11+
12+
// Content
13+
private let modalContent: () -> ModalContent
14+
15+
// Private
16+
@State private var dragOffset: CGSize = .zero
17+
18+
init(isPresented: Binding<Bool>, selection: Binding<Selection>, dismissable: Bool, onDismiss: (() -> Void)?, @ViewBuilder modalContent: @escaping () -> ModalContent) {
19+
self._isPresented = isPresented
20+
self._selection = selection
21+
self.dismissable = dismissable
22+
self.onDismiss = onDismiss
23+
self.modalContent = modalContent
24+
}
25+
26+
func body(content: Content) -> some View {
27+
ZStack {
28+
29+
content
30+
.zIndex(0)
31+
32+
if isPresented {
33+
34+
// Background
35+
Color.black.opacity(0.4)
36+
.ignoresSafeArea()
37+
.transition(.opacity)
38+
.animation(.easeOut, value: isPresented)
39+
.zIndex(1)
40+
41+
// Content
42+
VStack {
43+
Spacer()
44+
VStack {
45+
modalContent()
46+
.transitionBlurReplaceCombability()
47+
.padding(.top, Spaces.default_half + Spaces.default_more)
48+
.padding(.bottom, Spaces.default_more)
49+
.padding(.horizontal, Spaces.default_double)
50+
.overlay {
51+
if dismissable {
52+
VStack {
53+
MimicrateCloseButton {
54+
isPresented = false
55+
}
56+
Spacer()
57+
}
58+
.frame(maxWidth: .infinity, alignment: .trailing)
59+
.padding(Spaces.default_more)
60+
}
61+
}
62+
}
63+
.background {
64+
Color.white
65+
}
66+
.clipShape(.rect(cornerRadius: cornerRadius))
67+
.shadow(color: .black.opacity(0.12), radius: 6, x: .zero, y: 6)
68+
.shadow(color: .black.opacity(0.15), radius: 16, x: .zero, y: 12)
69+
.overlay {
70+
RoundedRectangle(cornerRadius: cornerRadius)
71+
.strokeBorder(.secondary.opacity(0.5), lineWidth: 1)
72+
}
73+
.padding(.horizontal, padding)
74+
.padding(.bottom, padding)
75+
.offset(y: calculateDragOffset)
76+
.gesture(
77+
DragGesture()
78+
.onChanged { gesture in
79+
dragOffset = gesture.translation
80+
}
81+
.onEnded { _ in
82+
if dragOffset.height > 100 && dismissable {
83+
isPresented = false
84+
} else {
85+
withAnimation(.interpolatingSpring(duration: 0.26)) {
86+
dragOffset = .zero
87+
}
88+
}
89+
}
90+
)
91+
}
92+
.ignoresSafeArea()
93+
.transition(.move(edge: .bottom))
94+
.zIndex(2)
95+
}
96+
}
97+
.animation(.smooth(duration: presentDimissDuration), value: isPresented)
98+
.animation(.default, value: selection)
99+
.onChange(of: isPresented) { isPresented in
100+
if !isPresented {
101+
delay(presentDimissDuration) {
102+
self.onDismiss?()
103+
}
104+
}
105+
}
106+
}
107+
108+
// MARK: - Private
109+
110+
private var calculateDragOffset: CGFloat {
111+
let dragDistance = dragOffset.height
112+
let calm = dragDistance < 0 || !dismissable
113+
114+
if calm {
115+
let squaredDistance = sqrt(abs(dragDistance))
116+
if dragDistance < 0 {
117+
return max(-squaredDistance * 3, dragDistance)
118+
} else {
119+
return min(squaredDistance * 2, dragDistance)
120+
}
121+
} else {
122+
return dragDistance
123+
}
124+
}
125+
126+
// MARK: - Constants
127+
128+
private var padding: CGFloat = 10
129+
private var cornerRadius: CGFloat { UIScreen.main.displayCornerRadius - padding }
130+
private var presentDimissDuration: TimeInterval { 0.41 }
131+
}

Sources/SwiftUIExtension/Views/Mimicrate/NativeSection.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import SwiftUI
2-
import SafeSFSymbols
32
import SwiftUIIntrospect
43

54
public struct NativeSection<Content: View, Detail: View>: View {
@@ -37,7 +36,7 @@ public struct NativeSection<Content: View, Detail: View>: View {
3736
.foregroundColor(.primary)
3837
.font(.title2)
3938
.fontWeightCompability(.bold)
40-
Image(.chevron.right)
39+
Image("chevron.right")
4140
.foregroundColor(.secondary)
4241
.font(.footnote)
4342
.fontWeightCompability(.heavy)

0 commit comments

Comments
 (0)