Skip to content

Instantly share code, notes, and snippets.

@1amageek
Created September 23, 2019 01:04
Show Gist options
  • Save 1amageek/1a5b896919fb27cfe37272a74dbec34e to your computer and use it in GitHub Desktop.
Save 1amageek/1a5b896919fb27cfe37272a74dbec34e to your computer and use it in GitHub Desktop.
SwiftUI ModalView sample
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