I have created a slider view which consists of 2 images, a chevron pointer and a slider line. My ultimate goal is to offset the button in the opposite direction to the chevron image so that when the user clicks the empty space the button will slide to its location. Below is a gif of me attempting to achieve this. My approach is quite manual and I suspect there is a sophisticated way of achieving my goal. I have it working when I click the button once, I suspect that on the other side I have my numbers wrong so it doesn't work when the image is on the left side of the screen.
import SwiftUI
extension Animation {
static func smooth() -> Animation {
Animation.spring()
.speed(0.7)
}
}
struct SliderView: View {
//this works as a toggle you can implement other booleans with this
#State var partyType: Bool = false
#State private var isOn = false
#State var width: CGFloat = 0
#State var height: CGFloat = 0
let screen = UIScreen.main.bounds
var body: some View {
ZStack {
Toggle("",isOn: $isOn).toggleStyle(SliderToggle())
}
}
func imageWidthX(name: String) -> CGFloat {
let image = UIImage(named: name)!
let width = image.size.width
return width * 2.5
}
func hostParty() -> Bool {
if partyType {
return true
} else {
return false
}
}
}
struct SlideEffect: GeometryEffect {
var offset: CGSize
var animatableData: CGSize.AnimatableData {
get { CGSize.AnimatableData(offset.width, offset.height) }
set { offset = CGSize(width: newValue.first, height: newValue.second) }
}
public func effectValue(size: CGSize) -> ProjectionTransform {
return ProjectionTransform(
CGAffineTransform(
translationX: offset.width,
y: offset.height
)
)
}
}
struct SliderView_Previews: PreviewProvider {
static var previews: some View {
SliderView()
}
}
struct SliderToggle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
GeometryReader{ geo in
VStack (alignment: .leading,spacing: 0){
Button(action: {
withAnimation() {
configuration.isOn.toggle()
}
}) {
Image("chevronPointer")
.clipped()
.animation(.smooth())
.padding(.horizontal, 30)
.offset(x: geo.size.width - 100, y: 0)
}
.modifier(
SlideEffect(
offset: CGSize(
width: configuration.isOn ? -geo.size.width + imageWidthX(name: "chevronPointer") : 0,
height: 0.0
)
)
)
Image("sliderLine")
.resizable()
.scaledToFit()
.frame(width: geo.size.width, alignment: .center)
}
}
.frame(height: 40)
}
}
func imageWidthX(name: String) -> CGFloat {
let image = UIImage(named: name)!
let width = image.size.width
return width * 2.5
}
I worded the question pretty poorly but ultimately the goal I wanted to achieve was to have 2 buttons on a stack that controlled the view under it. When the opposite side is clicked the slider icon will travel to that side and vice versa. The offset made things a lot more complicated than necessary so I removed this code and basically created invisible buttons. IF there is a more elegant way top achieve this I would be truly greatful
HStack {
ZStack {
Button (" ") {
withAnimation(){
configuration.isOn.toggle()
}}
.padding(.trailing).frame(width: 70, height: 25)
Image("chevronPointer")
.clipped()
.animation(.smooth())
.padding(.horizontal, 30)
//.offset(x: geo.size.width - 100, y: 0)
.modifier(SlideEffect(offset: CGSize(width: configuration.isOn ? geo.size.width - imageWidthX(name: "chevronPointer"): 0, height: 0.0)))
}
Spacer()
Button(" "){
withAnimation(){
configuration.isOn.toggle()
}
}.padding(.trailing).frame(width: 70, height: 25)
}
Related
So basically I need to come up with a layout that aligns the middle of a View to the bottom of other View in SwiftUI.
To make it more clear, all I need is something like this:
I guess the equivalent in a UIKit world would be:
redView.bottomAnchor.constraint(equalTo: whiteView.centerYAnchor)
Ive tried setting both in a ZStack and offsetting the white View but it won't work on all screen sizes for obvious reasons.
Any tips?
You can use .aligmentGuide (which is a tricky beast, I recommend this explanation)
Here is your solution, its independent of the child view sizes:
struct ContentView: View {
var body: some View {
ZStack(alignment: .bottom) { // subviews generally aligned at bottom
redView
whiteView
// center of this subview aligned to .bottom
.alignmentGuide(VerticalAlignment.bottom,
computeValue: { d in d[VerticalAlignment.center] })
}
.padding()
}
var redView: some View {
VStack {
Text("Red View")
}
.frame(maxWidth: .infinity)
.frame(height: 200)
.background(Color.red)
.cornerRadius(20)
}
var whiteView: some View {
VStack {
Text("White View")
}
.frame(maxWidth: .infinity)
.frame(width: 250, height: 100)
.background(Color.white)
.cornerRadius(20)
.overlay(RoundedRectangle(cornerRadius: 20).stroke())
}
}
You may try this:
ZStack {
Rectangle()
.frame(width: 300, height: 400)
.overlay(
GeometryReader { proxy in
let offsetY = proxy.frame(in: .named("back")).midY
Rectangle()
.fill(Color.red)
.offset(y: offsetY)
}
.frame(width: 150, height: 140)
, alignment: .center)
}
.coordinateSpace(name: "back")
Bascially the idea is to use coordinateSpace to get the frame of the bottom Rectangle and use geometryreader to get the offset needed by comparing the frame of top rectangle with the bottom one. Since we are using overlay and it is already aligned to the center horizontally, we just need to offset y to get the effect you want.
Based on OPs request in comment, this is a solution making use of a custom Layout.
The HalfOverlayLayout takes two subview and places the second half height over the first. The size of the first subview is flexible. As this is my first Layout I'm not sure if I covered all possible size variants, but it should be a start.
struct ContentView: View {
var body: some View {
HalfOverlayLayout {
redView
whiteView
}
.padding()
}
var redView: some View {
VStack {
ForEach(0..<20) { _ in
Text("Red View")
}
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.cornerRadius(20)
}
var whiteView: some View {
VStack {
Text("White View")
}
.frame(width: 200, height: 150)
.background(Color.white)
.cornerRadius(20)
.overlay(RoundedRectangle(cornerRadius: 20).stroke())
}
}
struct HalfOverlayLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let heightContent = subviews.first?.sizeThatFits(.unspecified).height ?? 0
let heightFooter = subviews.last?.sizeThatFits(.unspecified).height ?? 0
let totalHeight = heightContent + heightFooter / 2
let maxsizes = subviews.map { $0.sizeThatFits(.infinity) }
var totalWidth = maxsizes.max {$0.width < $1.width}?.width ?? 0
if let proposedWidth = proposal.width {
if totalWidth > proposedWidth { totalWidth = proposedWidth }
}
return CGSize(width: totalWidth, height: totalHeight)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let heightFooter = subviews.last?.sizeThatFits(.unspecified).height ?? 0
let maxHeightContent = bounds.height - heightFooter / 2
var pt = CGPoint(x: bounds.midX, y: bounds.minY)
if let first = subviews.first {
var totalWidth = first.sizeThatFits(.infinity).width
if let proposedWidth = proposal.width {
if totalWidth > proposedWidth { totalWidth = proposedWidth }
}
first.place(at: pt, anchor: .top, proposal: .init(width: totalWidth, height: maxHeightContent))
}
pt = CGPoint(x: bounds.midX, y: bounds.maxY)
if let last = subviews.last {
last.place(at: pt, anchor: .bottom, proposal: .unspecified)
}
}
}
Here's a main ContentView and DetailedCardView and when open DetailedCardView there's a ScrollView with more content. It also have closeGesture which allow to close the card when swiping from left to right and vice versa. So, at that point problems start to appear, like this ones inside DetailedCardView:
When scrolling in ScrollView then safeAreaInsets .top and .bottom start to be visible and as a result it's shrink the whole view vertically.
The closeGesture and standard ScrollView gesture while combined is not smooth and I'd like to add closing card not only when swiping horizontally but vertically too.
As a result I'd like to have the same gesture behaviour like opening and closing detailed card in App Store App Today section. Would love to get your help 🙌
ContentView DetailedCardView closeGesture in Action safeAreaInsets
struct ContentView: View {
#State private var showCardView = false
let colors: [Color] = [.red, .purple, .yellow, .green, .blue, .mint, .orange]
#State private var selectedCardNumber = 0
var body: some View {
ZStack {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(1..<101) { number in
colors[number % colors.count]
.overlay(Text("\(number)").font(.largeTitle.bold()).foregroundColor(.white))
.frame(height: 300)
.cornerRadius(30)
.padding(10)
.onTapGesture {
showCardView.toggle()
selectedCardNumber = number
}
}
}
}
.opacity(showCardView ? 0 : 1)
.animation(.spring(), value: showCardView)
if showCardView {
CardDetailView(showCardView: $showCardView, number: selectedCardNumber)
}
}
}
}
struct CardDetailView: View {
#Binding var showCardView: Bool
#State private var cardPosition: CGSize = .zero
let number: Int
var body: some View {
ScrollView {
RoundedRectangle(cornerRadius: 30, style: .continuous)
.fill(.green)
.overlay(Text("\(number)").font(.system(size: 100).bold()).foregroundColor(.white))
.frame(height: 500)
Color.red.opacity(0.8)
.frame(height: 200)
Color.gray.opacity(0.8)
.frame(height: 200)
Color.blue
.frame(height: 200)
Color.yellow
.frame(height: 200)
}
.scaleEffect(gestureScale())
.gesture(closeGesture)
}
private var closeGesture: some Gesture {
DragGesture()
.onChanged { progress in
withAnimation(.spring()) {
cardPosition = progress.translation
}
}
.onEnded { _ in
withAnimation(.spring()) {
let positionInTwoDirections = abs(cardPosition.width)
if positionInTwoDirections > 100 {
showCardView = false
}
cardPosition = .zero
}
}
}
private func gestureScale() -> CGFloat {
let max = UIScreen.main.bounds.width / 2
let currentAmount = abs(cardPosition.width)
let percentage = currentAmount / max
return 1 - min(percentage, 0.5)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I am creating a Cardview stored with user data for each user in the usersList. Now I want to offset each card for itself by pressing on either button. If I press the button now I will offset every card created by the foreach. How can I make the button only affect the first card? It is probably a better Idea to offset the card in the CardView itself, like I did with the Draggesture and not in the ContentView
struct ContentView: View {
#State var usersList: [User] = []
#State var fetchingComplete: Bool = false
#State var offset = CGSize.zero
#State var buttonPushed: Bool = false
var body: some View {
VStack {
NavBar().onAppear() {
fetchUsers()
}
if fetchingComplete {
ZStack {
ForEach(usersList, id: \.id) { user in
CardView(user: user).offset(x: offset.width, y: offset.height * 0.4)
.rotationEffect(.degrees(Double(offset.width / 40)))
.animation(.spring(), value: buttonPushed)
}
}
}
ButtonsBar(offset: $offset, buttonPushed: $buttonPushed)
}
}
}
public func fetchUsers() -> [User]{
FirebaseManager.shared.firestore.collection("users").getDocuments { snapshot, error in
if let error = error {
print("Failed to fetch users: ", error)
return
}
for document in snapshot!.documents {
let user = User(dictionary: document.data())
usersList.append(user)
}
fetchingComplete.toggle()
}
return usersList
}
}
struct ButtonsBar:View {
#Binding var offset: CGSize
#Binding var buttonPushed: Bool
var body: some View {
HStack {
Button {
print("perform dislike")
offset = CGSize(width: -500, height: 0)
buttonPushed.toggle()
} label: {
Image("dismiss_circle")
}.frame(width: 65)
Button {
print("perform like")
offset = CGSize(width: 500, height: 0)
buttonPushed.toggle()
} label: {
Image("like_circle")
}.frame(width: 65)
}
}
}
struct CardView: View {
#State private var offset = CGSize.zero
var user: User
var body: some View {
VStack{
Image("lady")
.resizable()
}.cornerRadius(12)
.frame(width: 310, height: 392)
.scaledToFit()
.overlay(ImageOverlay(name: self.user.name ?? "", age: self.user.age ?? 0, profession: self.user.profession ?? ""), alignment: .bottomLeading)
.offset(x: offset.width, y: offset.height * 0.4)
.rotationEffect(.degrees(Double(offset.width / 40)))
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
} .onEnded { _ in
withAnimation {
swipeCard(width: offset.width)
}
}
)
}
func swipeCard(width: CGFloat) {
switch width {
case -500...(-150):
offset = CGSize(width: -500, height: 0)
case 150...500:
offset = CGSize(width: 500, height: 0)
default:
offset = .zero
}
}
}
Right now, your current design will be unable to achieve what you're saying. How you have designed offset makes it so there's only the single source of truth being a single var offset. Thus, there's no way to give an offset to each card uniquely. Likewise, your design of a single ButtonBar will also be unable to achieve the goal of modifying each unique entry. How would this ButtonBar as it is choose any particular CardView? I know how to implement this, but I think you can figure it out once I explain more on how to uniquely associate values to each CardView.
I know you asked for how to just indent the first card only, but I don't think the easiest answer will be your desired outcome.
What you should do is that you have to track an offset and/or a buttonPressed for EACH user in your UserList. The easiest way is to create a CardViewModel so at the top of your CardView add
#StateObject var cardVM = CardViewModel()
and for the CardViewModel,
class CardViewModel : ObservableObject {
#Published var offset = CGSize.zero
#Published var buttonPushed: Bool = false
}
Now we have a unique offset and buttonPushed for each CardView. From here I would remove the modifiers
.offset(x: offset.width, y: offset.height * 0.4)
.rotationEffect(.degrees(Double(offset.width / 40)))
.animation(.spring(), value: buttonPushed)
and inside the CardView place your ButtonBar like so
struct CardView: View {
#StateObject var cardVM = CardViewModel()
var user: User
var body: some View {
VStack{
ButtonBar(offset: $cardVM.offset, buttonPushed: $cardVM.buttomPushed)
Image("lady")
.resizable()
}
.cornerRadius(12)
.frame(width: 310, height: 392)
.scaledToFit()
.overlay(ImageOverlay(name: self.user.name ?? "", age: self.user.age ?? 0, profession: self.user.profession ?? ""), alignment: .bottomLeading)
.offset(x: offset.width, y: offset.height * 0.4)
.rotationEffect(.degrees(Double(offset.width / 40)))
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
}
.onEnded { _ in
withAnimation {
swipeCard(width: offset.width)
}
}
)
// the offset and effects based on button push
.offset(x: cardVM.offset.width, y: cardVM.offset.height * 0.4)
.rotationEffect(.degrees(Double(cardVM.offset.width / 40)))
.animation(.spring(), value: cardVM.buttonPushed)
}
func swipeCard(width: CGFloat) {
switch width {
case -500...(-150):
offset = CGSize(width: -500, height: 0)
case 150...500:
offset = CGSize(width: 500, height: 0)
default:
offset = .zero
}
}
}
Because this isn't an MRE, I'm not able to actually test this, but in general, this should follow the design of creating a CardViewModel for each CardView which contains unique to each view variables offset and buttonPressed and placing the ButtonBar in each CardView allows us to modify the CardViewModel uniquely too.
EDIT:
In your main View, create
#StateObject var buttonBarVM = ButtonBarViewModel()
which has a
#Published var cardVM = CardViewModel().
In CardView, do
#EnvironmentObject var buttonBarVM: ButtonBarViewModel and
.onTap of the CardView or whatever gesture to do self.ButtonBarVM.cardVM = self.cardVM
In ButtonBar, instantiate
#EnvironmentObject var buttonBarVM: ButtonBarViewModel
and change the buttons to modify self.buttonBarVM.cardVM.(states)
One thing I would do is also pass #Published var user: User into cardVM and so when everything is first instantiated, you can wrap the ButtonBar in an if to only show when the user in cardVM is not default because at the moment, unless you first set the ButtonBar, the actions won't do anything until you gesture a CardView.
Here, we create a barButtonsVM to be our controller of a cardVM that you need to select, and we can set that based on a gesture you do to a card. We must preserve #StateObject cardVM = CardViewModel() because that specifically creates a ViewModel for each CardView.
I would like to allow a user to crop images by drawing a closed shape on the original image. Sort of like Snapchat stickers. I was looking at this post from 2012 and was curious if there is an updated way to do this with SwiftUI.
To clarify, I would like to take user input (drawing) over a displayed image and crop the actual image (not the view) with a mask that is that shape.
Actually your question need to more specific.
but you can resize your image by following code in swift ui.
Image("Your Image name here!")
.resizable()
.frame(width: 300, height: 300)
if you want to crop at any position of the image you may try this one
import SwiftUI
struct ContentView: View {
var body: some View {
CropImage(imageName: "Your Image name here!")
}
}
// struct Shape
struct CropFrame: Shape {
let isActive: Bool
func path(in rect: CGRect) -> Path {
guard isActive else { return Path(rect) } // full rect for non active
let size = CGSize(width: UIScreen.main.bounds.size.width*0.7, height: UIScreen.main.bounds.size.height/5)
let origin = CGPoint(x: rect.midX - size.width/2, y: rect.midY - size.height/2)
return Path(CGRect(origin: origin, size: size).integral)
}
}
// struct image crop
struct CropImage: View {
let imageName: String
#State private var currentPosition: CGSize = .zero
#State private var newPosition: CGSize = .zero
#State private var clipped = false
var body: some View {
VStack {
ZStack {
Image(imageName)
.resizable()
.scaledToFit()
.offset(x: currentPosition.width, y: currentPosition.height)
Rectangle()
.fill(Color.black.opacity(0.3))
.frame(width: UIScreen.main.bounds.size.width*0.7 , height: UIScreen.main.bounds.size.height/5)
.overlay(Rectangle().stroke(Color.white, lineWidth: 3))
}
.clipShape(
CropFrame(isActive: clipped)
)
.gesture(DragGesture()
.onChanged { value in
currentPosition = CGSize(width: value.translation.width + newPosition.width, height: value.translation.height + newPosition.height)
}
.onEnded { value in
currentPosition = CGSize(width: value.translation.width + newPosition.width, height: value.translation.height + newPosition.height)
newPosition = currentPosition
})
Button (action : { clipped.toggle() }) {
Text("Crop Image")
.padding(.all, 10)
.background(Color.blue)
.foregroundColor(.white)
.shadow(color: .gray, radius: 1)
.padding(.top, 50)
}
}
}
}
This is the full code of a view with image cropping.
else you may use a library from GitHub, see the GitHub demo here
I have 25 buttons laid out in a grid for a game like this.
I'm generating these buttons like this:
VStack {
ForEach(1..<6) {_ in
HStack {
ForEach(1..<6) {_ in
Button(action: {
// Button clicked
doButton()
}) {
Rectangle()
.frame(width: 50, height: 50)
.border(Color.black, lineWidth: 1)
}
}
}
}
}
How can I make it so if the user drags their finger across multiple buttons, each one of the buttons are clicked, and doButton() is executed?
I tried using a DragGesture() but couldn't get it to work...
Swift 5.1, iOS 13.
I don't have a 100% solution for you, but I do have something to get you off the ground hopefully. These are not buttons, but simply squares. As I click/drag across them they^ll signal I passed by.
It isn't perfect, but should give you some thing more to work with.
I read a tutorial on medium on doing something similar with drag and drop. This one.
Can help but think you'll need to do something with drag here and the Geometry reader ultimately, detecting where they are and responding to that.
import SwiftUI
struct ContentView: View {
var body: some View {
return VStack {
ForEach(1..<6) {colY in
HStack {
ForEach(1..<6) {rowX in
boxView(row: rowX, col: colY)
}
}
}
}
}
}
struct boxView: View {
#State var row: Int
#State var col: Int
var body: some View {
let dragGesture = DragGesture(minimumDistance: 0, coordinateSpace: CoordinateSpace.global)
.onChanged { (value) in
print("trigger ",self.row,self.col)
}
return Rectangle()
.stroke(Color.black)
.frame(width: 50, height: 50)
.gesture(dragGesture)
}
}
import SwiftUI
class Container : ObservableObject {
#Published var location = CGPoint()
public func updateLocation(_ location: CGPoint) {
self.location = location
}
}
struct SliderGestureButtonsView: View {
#ObservedObject var container = Container()
var body: some View {
VStack(alignment: .center, spacing: 0) {
ForEach(1..<6) { colY in
HStack {
ForEach(1..<6) { rowX in
GeometryReader { gp in
let x = gp.frame(in:.named("hive")).midX
let y = gp.frame(in:.named("hive")).midY
let w = gp.frame(in:.named("hive")).size.width
let h = gp.frame(in:.named("hive")).size.height
let rect = CGRect(x: x, y: y, width: w, height: h)
boxView(row: rowX, col: colY, rect: rect, container: container)
}
}
}.frame(height: 60)
}
}.coordinateSpace(name: "hive").frame(width: 300)
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: CoordinateSpace.named("hive"))
.onChanged { (value) in
container.updateLocation(value.location)
}
)
}
}
struct boxView: View {
var row: Int
var col: Int
var rect: CGRect
#ObservedObject var container: Container
var body: some View {
let x = container.location.x
let y = container.location.y
let hw = rect.size.width / 2
let ox = rect.origin.x
let withinX = x > ox - hw && x < ox + hw
let hh = rect.size.height / 2
let oy = rect.origin.y
let withinY = y > oy - hh && y < oy + hh
var pressed = false
if(withinX && withinY) {
pressed = true
print("trigger ", self.row, self.col)
}
return Rectangle()
.stroke(Color.white)
.frame(width: 60, height: 60)
.background(pressed ? Color.green : Color.black)
}
}