Created
September 23, 2019 01:04
-
-
Save 1amageek/1a5b896919fb27cfe37272a74dbec34e to your computer and use it in GitHub Desktop.
SwiftUI ModalView sample
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
import SwiftUI | |
enum ModalViewState: Equatable { | |
case closed | |
case original | |
case custom(height: CGFloat) | |
case dragging(height: CGFloat?, location: CGPoint, translation: CGSize) | |
} | |
protocol ModalView: View { | |
var state: ModalViewState { get set } | |
var frame: CGRect { get set } | |
init(frame: CGRect, state: ModalViewState) | |
} | |
extension ModalView { | |
var height: CGFloat? { | |
if case .dragging(let height , _, let translation) = self.state { | |
let newHieght = (height ?? self.frame.height) - translation.height | |
return max(newHieght, 0) | |
} else if case .custom(let height) = self.state { | |
return height | |
} | |
return nil | |
} | |
var offset: CGSize { | |
if case .closed = self.state { | |
return CGSize(width: 0, height: UIScreen.main.bounds.height) | |
} | |
return .zero | |
} | |
var location: CGPoint? { | |
if case .dragging(_, let location, _) = self.state { | |
return location | |
} | |
return nil | |
} | |
var translation: CGSize? { | |
if case .dragging(_, _, let translation) = self.state { | |
return translation | |
} | |
return nil | |
} | |
} | |
extension View { | |
func modal<Content>(modalView: Binding<Content>, onDragGestureEnded: @escaping (Content) -> CGFloat?) -> some View where Content: ModalView { | |
ZStack { | |
Rectangle() | |
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height) | |
.background(Color.black) | |
.edgesIgnoringSafeArea(.vertical) | |
AnyView(self) | |
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height) | |
.background(Color.blue) | |
.cornerRadius(32) | |
.animation(.spring()) | |
.scaleEffect(modalView.wrappedValue.state == .closed ? 1 : 0.915) | |
.edgesIgnoringSafeArea(.vertical) | |
Rectangle() | |
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height) | |
.background(Color.black) | |
.animation(.spring()) | |
.opacity(modalView.wrappedValue.state == .closed ? 0 : 0.2) | |
.edgesIgnoringSafeArea(.vertical) | |
VStack { | |
Spacer(minLength: 0) | |
modalView.wrappedValue | |
.background(GeometryReader { proxy -> AnyView in | |
let rect = proxy.frame(in: .global) | |
if modalView.wrappedValue.translation == nil, rect.integral != modalView.wrappedValue.frame.integral { | |
DispatchQueue.main.async { | |
modalView.wrappedValue = Content(frame: rect, state: modalView.wrappedValue.state) | |
} | |
} | |
return AnyView(EmptyView()) | |
}) | |
.frame(height: modalView.wrappedValue.height) | |
.background(Color(UIColor.systemBackground)) | |
.edgesIgnoringSafeArea(.vertical) | |
.offset(modalView.wrappedValue.offset) | |
.animation(.spring()) | |
.gesture( | |
DragGesture(coordinateSpace: .global) | |
.onChanged { value in | |
if case .dragging(let height , _, _) = modalView.wrappedValue.state { | |
modalView.wrappedValue = Content(frame: modalView.wrappedValue.frame, state: .dragging(height: height, location: value.location, translation: value.translation)) | |
} else if case .custom(let height) = modalView.wrappedValue.state { | |
modalView.wrappedValue = Content(frame: modalView.wrappedValue.frame, state: .dragging(height: height, location: value.location, translation: value.translation)) | |
} else { | |
modalView.wrappedValue = Content(frame: modalView.wrappedValue.frame, state: .dragging(height: nil, location: value.location, translation: value.translation)) | |
} | |
}.onEnded { value in | |
if let height = onDragGestureEnded(modalView.wrappedValue) { | |
modalView.wrappedValue = Content(frame: modalView.wrappedValue.frame, state: .custom(height: height)) | |
} else { | |
modalView.wrappedValue = Content(frame: modalView.wrappedValue.frame, state: .closed) | |
} | |
}) | |
} | |
} | |
.edgesIgnoringSafeArea(.bottom) | |
} | |
} | |
struct AnyModalView: ModalView { | |
var state: ModalViewState | |
var frame: CGRect | |
init(frame: CGRect = .zero, state: ModalViewState = .closed) { | |
self.frame = frame | |
self.state = state | |
} | |
var body: some View { | |
Text("ModalView") | |
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: 200) | |
.background(Color.white) | |
.edgesIgnoringSafeArea(.bottom) | |
} | |
} | |
struct ContentView: View { | |
@State var modalView: AnyModalView = AnyModalView() | |
var body: some View { | |
VStack { | |
Button("Show") { | |
self.modalView = AnyModalView(state: .original) | |
} | |
.foregroundColor(Color.white) | |
} | |
.modal(modalView: $modalView) { state in | |
return 320 | |
} | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static var previews: some View { | |
ContentView() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment