Last active
October 10, 2020 01:22
-
-
Save CastIrony/e7db0de2e8e88d36c82b50cf79f6d887 to your computer and use it in GitHub Desktop.
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
// | |
// ContentView.swift | |
// | |
// | |
// Created by Bernstein, Joel on 7/4/20. | |
// | |
import SwiftUI | |
struct ElementModel: Identifiable | |
{ | |
let id: Int | |
var frame: CGRect | |
} | |
struct ContentView: View | |
{ | |
@State var elements: [ElementModel] = | |
[ | |
ElementModel(id: 1, frame: CGRect(x: 50, y: 220, width: 190, height: 250)), | |
ElementModel(id: 2, frame: CGRect(x: 100, y: 270, width: 190, height: 250)), | |
ElementModel(id: 3, frame: CGRect(x: 150, y: 320, width: 190, height: 250)), | |
] | |
@State var selectedID: Int? | |
var body: some View | |
{ | |
ZStack | |
{ | |
Color.green.edgesIgnoringSafeArea(.all) | |
.onTapGesture { selectedID = nil } | |
ForEach(elements.indices) | |
{ | |
elementIndex in | |
let isSelectedBinding = Binding<Bool> | |
{ | |
selectedID == elements[elementIndex].id | |
} | |
set: | |
{ | |
newValue in | |
if(newValue) | |
{ | |
selectedID = elements[elementIndex].id | |
} | |
else if selectedID == elements[elementIndex].id | |
{ | |
selectedID = nil | |
} | |
} | |
RoundedRectangle(cornerRadius: 15.0, style: .continuous) | |
.fill(Color.white) | |
.overlay(VStack(spacing: 15) | |
{ | |
Text("Hello World!").font(.system(.largeTitle)) | |
Text("Foo").font(.system(.headline)) | |
Text("Bar").font(.system(.caption)) | |
}) | |
.shadow(radius: 20, y: 20) | |
.modifier(DragResizable(frame: $elements[elementIndex].frame, isSelected: isSelectedBinding)) | |
} | |
} | |
} | |
} | |
struct ContentView_Previews: PreviewProvider | |
{ | |
static var previews: some View | |
{ | |
Group | |
{ | |
ContentView() | |
} | |
} | |
} | |
struct DragResizable: ViewModifier | |
{ | |
@Binding var frame: CGRect | |
@Binding var isSelected: Bool | |
@State var initialDragDelta: CGSize? | |
@State var isDragging = false | |
func body(content: Content) -> some View | |
{ | |
ZStack | |
{ | |
content | |
.frame(width: frame.width, height: frame.height, alignment: .center) | |
.position(CGPoint(x: frame.midX, y: frame.midY)) | |
.gesture(DragGesture(minimumDistance: 2, coordinateSpace: .global) | |
.onChanged | |
{ | |
value in | |
if initialDragDelta == nil | |
{ | |
initialDragDelta = CGSize(width: value.startLocation.x - frame.midX, height: value.startLocation.y - frame.midY) | |
isDragging = true | |
isSelected = true | |
} | |
updateDragPosition(newValue: value.location) | |
} | |
.onEnded | |
{ | |
value in | |
initialDragDelta = nil | |
isDragging = false | |
} | |
.simultaneously(with: TapGesture().onEnded { isSelected.toggle() })) | |
if(isSelected) | |
{ | |
HandleBorder(frame: $frame, color: .black, width: 8) | |
if(isDragging) | |
{ | |
Rectangle() | |
.fill(Color.gray) | |
.opacity(0.2) | |
.frame(width: frame.width, height: frame.height, alignment: .center) | |
.position(CGPoint(x: frame.midX, y: frame.midY)) | |
.allowsHitTesting(false) | |
} | |
Group() | |
{ | |
HandleView(frame: $frame, anchorPostion:Alignment(horizontal: .leading, vertical: .top)) | |
HandleView(frame: $frame, anchorPostion:Alignment(horizontal: .center, vertical: .top)) | |
HandleView(frame: $frame, anchorPostion:Alignment(horizontal: .trailing, vertical: .top)) | |
HandleView(frame: $frame, anchorPostion:Alignment(horizontal: .leading, vertical: .center)) | |
HandleView(frame: $frame, anchorPostion:Alignment(horizontal: .trailing, vertical: .center)) | |
HandleView(frame: $frame, anchorPostion:Alignment(horizontal: .leading, vertical: .bottom)) | |
HandleView(frame: $frame, anchorPostion:Alignment(horizontal: .center, vertical: .bottom)) | |
HandleView(frame: $frame, anchorPostion:Alignment(horizontal: .trailing, vertical: .bottom)) | |
} | |
HandleBorder(frame: $frame, color: .white, width: 4) | |
} | |
} | |
} | |
func updateDragPosition(newValue: CGPoint) | |
{ | |
let midX = newValue.x - (initialDragDelta?.width ?? 0) | |
let midY = newValue.y - (initialDragDelta?.height ?? 0) | |
let minX = midX - frame.width / 2 | |
let minY = midY - frame.height / 2 | |
frame = CGRect(x: minX, y: minY, width: frame.width, height: frame.height) | |
} | |
} | |
struct HandleBorder: View | |
{ | |
@Binding var frame: CGRect | |
let color:Color | |
let width:CGFloat | |
var body: some View | |
{ | |
ZStack | |
{ | |
Rectangle() | |
.stroke(color, lineWidth: width) | |
.frame(width: frame.width, height: frame.height, alignment: .center) | |
.position(CGPoint(x: frame.midX, y: frame.midY)) | |
} | |
} | |
} | |
struct HandleView: View | |
{ | |
@Binding var frame: CGRect | |
@State var initialDragDelta: CGSize? | |
@State var isDragging = false | |
let anchorPostion: Alignment | |
let borderThickness: CGFloat = 2 | |
let handleSizeRegular: CGFloat = 12 | |
let handleSizeDragging: CGFloat = 50 | |
var handleSize: CGFloat { isDragging ? handleSizeDragging : handleSizeRegular } | |
var handlePosition: CGPoint | |
{ | |
var x: CGFloat | |
var y: CGFloat | |
switch anchorPostion.horizontal | |
{ | |
case .leading: x = frame.minX | |
case .trailing: x = frame.maxX | |
default: x = frame.midX | |
} | |
switch anchorPostion.vertical | |
{ | |
case .top: y = frame.minY | |
case .bottom: y = frame.maxY | |
default: y = frame.midY | |
} | |
return CGPoint(x: x, y: y) | |
} | |
func updateHandlePosition(newValue: CGPoint) | |
{ | |
var minX = frame.minX | |
var maxX = frame.maxX | |
var minY = frame.minY | |
var maxY = frame.maxY | |
switch anchorPostion.horizontal | |
{ | |
case .leading: minX = newValue.x - (initialDragDelta?.width ?? 0) | |
case .trailing: maxX = newValue.x - (initialDragDelta?.width ?? 0) | |
default: break | |
} | |
switch anchorPostion.vertical | |
{ | |
case .top: minY = newValue.y - (initialDragDelta?.height ?? 0) | |
case .bottom: maxY = newValue.y - (initialDragDelta?.height ?? 0) | |
default: break | |
} | |
frame = CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) | |
} | |
var body: some View | |
{ | |
ZStack | |
{ | |
RoundedRectangle(cornerRadius: (isDragging ? 27 : 4), style: .continuous) | |
.fill(Color.black) | |
.frame(width: handleSize + 2 * borderThickness, height: handleSize + 2 * borderThickness, alignment: .center) | |
RoundedRectangle(cornerRadius: (isDragging ? 25 : 2), style: .continuous) | |
.fill(Color.white) | |
.frame(width: handleSize, height: handleSize, alignment: .center) | |
} | |
.frame(width: 60, height: 60, alignment: .center) | |
.contentShape(Rectangle()) | |
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global) | |
.onChanged | |
{ | |
value in | |
if initialDragDelta == nil | |
{ | |
initialDragDelta = CGSize(width: value.startLocation.x - handlePosition.x, height: value.startLocation.y - handlePosition.y) | |
withAnimation(.spring(response: 0.3, dampingFraction:0.3)) { isDragging = true } | |
} | |
updateHandlePosition(newValue: value.location) | |
} | |
.onEnded | |
{ | |
value in | |
initialDragDelta = nil | |
withAnimation(.spring(response: 0.3, dampingFraction:0.6)) { isDragging = false } | |
}) | |
.position(handlePosition) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment