-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added modal sheet view and fixed existing objects.
- Loading branch information
1 parent
37530ca
commit 32acaaa
Showing
8 changed files
with
324 additions
and
3 deletions.
There are no files selected for viewing
21 changes: 21 additions & 0 deletions
21
Sources/SwiftUIExtension/Compability/TransitionBlurReplace.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import SwiftUI | ||
|
||
extension View { | ||
|
||
public func transitionBlurReplaceCombability() -> some View { | ||
modifier(TransitionBlurReplace()) | ||
} | ||
} | ||
|
||
struct TransitionBlurReplace: ViewModifier { | ||
|
||
func body(content: Content) -> some View { | ||
if #available(iOS 18.0, *) { | ||
content | ||
.transition(.blurReplace.combined(with: .opacity)) | ||
} else { | ||
content | ||
.transition(.opacity) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
64 changes: 64 additions & 0 deletions
64
Sources/SwiftUIExtension/Views/Mimicrate/LargeButton.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import SwiftUI | ||
|
||
extension ButtonStyle where Self == LargeButtonStyle { | ||
|
||
public static func large(_ colorise: ButtonColorise) -> Self { | ||
return LargeButtonStyle(colorise) | ||
} | ||
} | ||
|
||
public struct LargeButtonStyle: ButtonStyle { | ||
|
||
@Environment(\.isEnabled) private var isEnabled: Bool | ||
|
||
let colorise: ButtonColorise | ||
|
||
init(_ colorise: ButtonColorise) { | ||
self.colorise = colorise | ||
} | ||
|
||
public func makeBody(configuration: Configuration) -> some View { | ||
configuration.label | ||
.font(.headline) | ||
.foregroundColor(colorise.foregroundColor) | ||
.frame(maxWidth: .infinity) | ||
.padding(.vertical, 15) | ||
.padding(.horizontal, 10) | ||
.background { | ||
colorise.backgroundColor | ||
.clipShape(.rect(cornerRadius: 12)) | ||
} | ||
.opacity(configuration.isPressed ? 0.7 : 1) | ||
.animation(.default, value: configuration.isPressed) | ||
.opacity(isEnabled ? 1 : 0.4) | ||
.animation(.default, value: isEnabled) | ||
} | ||
} | ||
|
||
public enum ButtonColorise { | ||
|
||
case colorful | ||
case tinted | ||
case grayed | ||
|
||
var foregroundColor: Color { | ||
switch self { | ||
case .colorful: | ||
return .white | ||
case .tinted: | ||
return .accentColor | ||
case .grayed: | ||
return .accentColor | ||
} | ||
} | ||
var backgroundColor: Color { | ||
switch self { | ||
case .colorful: | ||
return .accentColor | ||
case .tinted: | ||
return .accentColor.opacity(0.12) | ||
case .grayed: | ||
return .gray.opacity(0.15) | ||
} | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
Sources/SwiftUIExtension/Views/Mimicrate/MimicrateCloseButton.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import SwiftUI | ||
|
||
struct MimicrateCloseButton: View { | ||
|
||
let action: () -> Void | ||
|
||
var body: some View { | ||
Button { | ||
action() | ||
} label: { | ||
Image(systemName: "xmark") | ||
.font(.footnote.bold()) | ||
.foregroundColor(.gray) | ||
.padding(7) | ||
.background { | ||
Circle() | ||
} | ||
.tint(.gray.opacity(0.2)) | ||
} | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
Sources/SwiftUIExtension/Views/Mimicrate/ModalSheet/ModalSheet.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import SwiftUI | ||
|
||
extension View { | ||
|
||
public func modalSheet<Content, Selection: Hashable>( | ||
isPresented: Binding<Bool>, | ||
selection: Binding<Selection>, | ||
dismissable: Bool, | ||
onDismiss: (() -> Void)? = nil, | ||
@ViewBuilder content: @escaping () -> Content | ||
) -> some View where Content : View { | ||
|
||
let sheet = ModalSheetModifier( | ||
isPresented: isPresented, | ||
selection: selection, | ||
dismissable: dismissable, | ||
onDismiss: onDismiss, | ||
modalContent: content | ||
) | ||
|
||
return self.modifier(sheet) | ||
} | ||
} | ||
|
61 changes: 61 additions & 0 deletions
61
Sources/SwiftUIExtension/Views/Mimicrate/ModalSheet/ModalSheetContentView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import SwiftUI | ||
|
||
/** | ||
Idea have to reusable basic style of modal sheet content with title+descrition & action button. | ||
*/ | ||
public struct ModalSheetContentView<Content: View, ActionContent: View>: View { | ||
|
||
let title: String | ||
let description: String? | ||
let content: () -> Content | ||
let actionContent: () -> ActionContent | ||
let actionEnabled: Bool | ||
let action: () -> Void | ||
let dismissable: Bool | ||
|
||
public init( | ||
title: String, | ||
description: String?, | ||
@ViewBuilder content: @escaping () -> Content, | ||
@ViewBuilder actionContent: @escaping () -> ActionContent, | ||
actionEnabled: Bool, | ||
action: @escaping () -> Void, | ||
dismissable: Bool | ||
) { | ||
self.title = title | ||
self.description = description | ||
self.content = content | ||
self.actionContent = actionContent | ||
self.actionEnabled = actionEnabled | ||
self.action = action | ||
|
||
self.dismissable = dismissable | ||
} | ||
|
||
public var body: some View { | ||
VStack(alignment: .center, spacing: .zero) { | ||
|
||
VStack(alignment: .center) { | ||
Text(title) | ||
.font(.title.weight(.bold)) | ||
|
||
if let description { | ||
Text(description) | ||
} | ||
} | ||
|
||
FixedSpacer(height: Spaces.default_double) | ||
content() | ||
FixedSpacer(height: Spaces.default_double) | ||
|
||
Button { | ||
action() | ||
} label: { | ||
actionContent() | ||
} | ||
.buttonStyle(.large(.tinted)) | ||
.disabled(!actionEnabled) | ||
} | ||
.multilineTextAlignment(.center) | ||
} | ||
} |
131 changes: 131 additions & 0 deletions
131
Sources/SwiftUIExtension/Views/Mimicrate/ModalSheet/ModalSheetModifier.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import SwiftUI | ||
import SwiftBoost | ||
|
||
struct ModalSheetModifier<ModalContent: View, Selection: Hashable>: ViewModifier { | ||
|
||
// States | ||
@Binding private var isPresented: Bool | ||
@Binding private var selection: Selection | ||
private let dismissable: Bool | ||
private let onDismiss: (() -> Void)? | ||
|
||
// Content | ||
private let modalContent: () -> ModalContent | ||
|
||
// Private | ||
@State private var dragOffset: CGSize = .zero | ||
|
||
init(isPresented: Binding<Bool>, selection: Binding<Selection>, dismissable: Bool, onDismiss: (() -> Void)?, @ViewBuilder modalContent: @escaping () -> ModalContent) { | ||
self._isPresented = isPresented | ||
self._selection = selection | ||
self.dismissable = dismissable | ||
self.onDismiss = onDismiss | ||
self.modalContent = modalContent | ||
} | ||
|
||
func body(content: Content) -> some View { | ||
ZStack { | ||
|
||
content | ||
.zIndex(0) | ||
|
||
if isPresented { | ||
|
||
// Background | ||
Color.black.opacity(0.4) | ||
.ignoresSafeArea() | ||
.transition(.opacity) | ||
.animation(.easeOut, value: isPresented) | ||
.zIndex(1) | ||
|
||
// Content | ||
VStack { | ||
Spacer() | ||
VStack { | ||
modalContent() | ||
.transitionBlurReplaceCombability() | ||
.padding(.top, Spaces.default_half + Spaces.default_more) | ||
.padding(.bottom, Spaces.default_more) | ||
.padding(.horizontal, Spaces.default_double) | ||
.overlay { | ||
if dismissable { | ||
VStack { | ||
MimicrateCloseButton { | ||
isPresented = false | ||
} | ||
Spacer() | ||
} | ||
.frame(maxWidth: .infinity, alignment: .trailing) | ||
.padding(Spaces.default_more) | ||
} | ||
} | ||
} | ||
.background { | ||
Color.white | ||
} | ||
.clipShape(.rect(cornerRadius: cornerRadius)) | ||
.shadow(color: .black.opacity(0.12), radius: 6, x: .zero, y: 6) | ||
.shadow(color: .black.opacity(0.15), radius: 16, x: .zero, y: 12) | ||
.overlay { | ||
RoundedRectangle(cornerRadius: cornerRadius) | ||
.strokeBorder(.secondary.opacity(0.5), lineWidth: 1) | ||
} | ||
.padding(.horizontal, padding) | ||
.padding(.bottom, padding) | ||
.offset(y: calculateDragOffset) | ||
.gesture( | ||
DragGesture() | ||
.onChanged { gesture in | ||
dragOffset = gesture.translation | ||
} | ||
.onEnded { _ in | ||
if dragOffset.height > 100 && dismissable { | ||
isPresented = false | ||
} else { | ||
withAnimation(.interpolatingSpring(duration: 0.26)) { | ||
dragOffset = .zero | ||
} | ||
} | ||
} | ||
) | ||
} | ||
.ignoresSafeArea() | ||
.transition(.move(edge: .bottom)) | ||
.zIndex(2) | ||
} | ||
} | ||
.animation(.smooth(duration: presentDimissDuration), value: isPresented) | ||
.animation(.default, value: selection) | ||
.onChange(of: isPresented) { isPresented in | ||
if !isPresented { | ||
delay(presentDimissDuration) { | ||
self.onDismiss?() | ||
} | ||
} | ||
} | ||
} | ||
|
||
// MARK: - Private | ||
|
||
private var calculateDragOffset: CGFloat { | ||
let dragDistance = dragOffset.height | ||
let calm = dragDistance < 0 || !dismissable | ||
|
||
if calm { | ||
let squaredDistance = sqrt(abs(dragDistance)) | ||
if dragDistance < 0 { | ||
return max(-squaredDistance * 3, dragDistance) | ||
} else { | ||
return min(squaredDistance * 2, dragDistance) | ||
} | ||
} else { | ||
return dragDistance | ||
} | ||
} | ||
|
||
// MARK: - Constants | ||
|
||
private var padding: CGFloat = 10 | ||
private var cornerRadius: CGFloat { UIScreen.main.displayCornerRadius - padding } | ||
private var presentDimissDuration: TimeInterval { 0.41 } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters