ScrollView limits DragGesture animation - swiftui

I have HStack with some images shown via ForEach view. Each image has DragGesture applied. I can drag an image all over the screen and the animation shown correctly. But when I put my HStack with images into the ScrollView when I drag an image (not scroll) the animation of gragging shown only within the ScrollView area. How can I make it show within the whole screen again?
import SwiftUI
struct SwiftUIView: View {
#State var position = CGSize.zero
#GestureState var dragOffset: [CGSize]
init() {
let dragOffsets = [CGSize](repeating: CGSize.zero, count: 36)
_dragOffset = GestureState(wrappedValue: dragOffsets)
}
var body: some View {
ScrollView(.horizontal) {
HStack(alignment: .center, spacing: 0) {
ForEach ((0..<player.playersCards.count), id: \.self) { number in
Image(player.playersCards[number].pic)
.resizable()
.frame(width: 93, height: 127)
.modifier(CardStyle())
.offset(dragOffset[number])
.gesture(
DragGesture(coordinateSpace: .global)
.updating($dragOffset, body: { (value, state, transaction) in
state[number] = value.translation
})
)
.animation(.spring())
}
}
}.offset(x: 15, y: 0)
}
}

You just need to pack your image into another view (group or ZStack)! Set the height of this view to the height of the screen, then the image will move inside the parent view:
struct SwiftUIView: View {
private let colors = [Color.blue, Color.yellow, Color.orange, Color.gray, Color.black, Color.green, Color.white]
#State var position = CGSize.zero
#GestureState var dragOffset: [CGSize]
init() {
let dragOffsets = [CGSize](repeating: CGSize.zero, count: 36)
_dragOffset = GestureState(wrappedValue: dragOffsets)
}
var body: some View {
ScrollView(.horizontal) {
HStack(alignment: .center, spacing: 0) {
ForEach (Array(0...6), id: \.self) { number in
ZStack {
Text("\(number.description)")
.frame(width: 93, height: 127)
.background(colors[number])
.offset(dragOffset[number])
.gesture(
DragGesture(coordinateSpace: .global)
.updating($dragOffset, body: { (value, state, transaction) in
state[number] = value.translation
})
)
.animation(.spring())
}
.frame(width: 93, height: UIScreen.main.bounds.height)
}
}
}
.background(Color.red)
}
}

Related

Position an image randomly on a view in swiftui and pass the position of the image to another fullscreen transparent view

I have a view in SwiftUI. This view has some random images on it in various random positions. Check the code below.
struct ContentView: View {
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
var body: some View {
ZStack {
ForEach(0..<5) { _ in
Image(systemName: "plus")
.frame(width: 30, height: 30)
.background(Color.green)
.position(
x: CGFloat.random(in: 0..<screenWidth),
y: CGFloat.random(in: 0..<screenHeight)
)
}
}
.ignoreSafeArea()
}
}
I need to get the exact position of these random added images and pass the positions to another transparent view that shows up with a ZStack on top of the previous view. In the transparent popup fullscreen ZStack view i need to point to the position of the images i randomly put in the previous view using arrow images. Is this somehow possible in swiftui? I am new in swiftui so any help or suggestion appreciated.
Store the random offsets in a #State var and generate them in .onAppear { }. Then you can use them to position the random images and pass the offsets to the overlay view:
struct ContentView: View {
#State private var imageOffsets: [CGPoint] = Array(repeating: CGPoint.zero, count: 5)
#State private var showingOverlay = true
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
var body: some View {
ZStack {
ForEach(0..<5) { index in
Image(systemName: "plus")
.frame(width: 30, height: 30)
.background(Color.green)
.position(
x: imageOffsets[index].x,
y: imageOffsets[index].y
)
}
}
.ignoresSafeArea()
.onAppear {
for index in 0..<5 {
imageOffsets[index] = CGPoint(x: .random(in: 0..<screenWidth), y: .random(in: 0..<screenHeight))
}
}
.overlay {
if showingOverlay {
OverlayView(imageOffsets: imageOffsets)
}
}
}
}
struct OverlayView: View {
let imageOffsets: [CGPoint]
var body: some View {
ZStack {
Color.clear
ForEach(0..<5) { index in
Circle()
.stroke(.blue)
.frame(width: 40, height: 40)
.position(
x: imageOffsets[index].x,
y: imageOffsets[index].y
)
}
}
.ignoresSafeArea()
}
}

Touch/drag motion to select multiple cells in a lazyvgrid?

I'm trying to use a LazyVGrid in SwiftUI where you can touch and drag your finger to select multiple adjacent cells in a specific order. This is not a drag and drop and I don't want to move the cells (maybe drag isn't the right term here, but couldn't think of another term to describe it). Also, you would be able to reverse the selection (ie: each cell can only be selected once and reversing direction would un-select the cell). How can I accomplish this? Thanks!
For example:
struct ContentView: View {
#EnvironmentObject private var cellsArray: CellsArray
var body: some View {
VStack {
LazyVGrid(columns: gridItems, spacing: spacing) {
ForEach(0..<(rows * columns), id: \.self){index in
VStack(spacing: 0) {
CellsView(index: index)
}
}
}
}
}
}
struct CellsView: View {
#State var index: Int
#EnvironmentObject var cellsArray: CellsArray
var body: some View {
ZStack {
Text("\(self.cellsArray[index].cellValue)") //cellValue is a string
.foregroundColor(Color.yellow)
.frame(width: getWidth(), height: getWidth())
.background(Color.gray)
}
//.onTapGesture ???
}
func getWidth()->CGFloat{
let width = UIScreen.main.bounds.width - 10
return width / CGFloat(columns)
}
}
Something like this might help, the only issue here is the coordinate space. Overlay is not drawing the rectangle in the correct coordinate space.
struct ImagesView: View {
var columnSize: CGFloat
#Binding var projectImages: [ProjectImage]
#State var selectedImages: [ImageSelection] = []
#State var dragWidth: CGFloat = 1.0
#State var dragHeight: CGFloat = 1.0
#State var dragStart: CGPoint = CGPoint(x:0, y:0)
let columns = [
GridItem(.adaptive(minimum: 200), spacing: 0)
]
var body: some View {
GeometryReader() { geometry in
ZStack{
ScrollView{
LazyVGrid(columns: [
GridItem(.adaptive(minimum: columnSize), spacing: 2)
], spacing: 2){
ForEach(projectImages, id: \.imageUUID){ image in
Image(nsImage: NSImage(data: image.imageData)!)
.resizable()
.scaledToFit()
.border(selectedImages.contains(where: {imageSelection in imageSelection.uuid == image.imageUUID}) ? Color.blue : .primary, width: selectedImages.contains(where: {imageSelection in imageSelection.uuid == image.imageUUID}) ? 5.0 : 1.0)
.gesture(TapGesture(count: 2).onEnded{
print("Double tap finished")
})
.gesture(TapGesture(count: 1).onEnded{
if selectedImages.contains(where: {imageSelection in imageSelection.uuid == image.imageUUID}) {
print("Image is already selected")
if let index = selectedImages.firstIndex(where: {imageSelection in imageSelection.uuid == image.imageUUID}){
selectedImages.remove(at: index)
}
} else {
selectedImages.append(ImageSelection(imageUUID: image.imageUUID))
print("Image has been selected")
}
})
}
}
.frame(minHeight: 50)
.padding(.horizontal, 1)
}
.simultaneousGesture(
DragGesture(minimumDistance: 2)
.onChanged({ value in
print("Drag Start: \(value.startLocation.x)")
self.dragStart = value.startLocation
self.dragWidth = value.translation.width
self.dragHeight = value.translation.height
})
)
.overlay{
Rectangle()
.frame(width: dragWidth, height: dragHeight)
.offset(x:dragStart.x, y:dragStart.y)
}
}
}
}
}

SwiftUI Crash when calling scrollTo method when view is disappearing

I try to make my ScrollView fixed in a specific place, but the scrollTo method will cause the application to crash.
How to make the ScrollView stay in a fixed place?
I want to control the switching of views by MagnificationGesture to switch from one view to another.
But when Scroll() disappdars, the app crashs.
struct ContentView: View {
#State var tabCount:Int = 1
#State var current:CGFloat = 1
#State var final:CGFloat = 1
var body: some View {
let magni = MagnificationGesture()
.onChanged(){ value in
current = value
}
.onEnded { value in
if current > 2 {
self.tabCount += 1
}
final = current + final
current = 0
}
VStack {
VStack {
Button("ChangeView"){
self.tabCount += 1
}
if tabCount%2 == 0 {
Text("some text")
}else {
Scroll(current: $current)
}
}
Spacer()
HStack {
Color.blue
}
.frame(width: 600, height: 100, alignment: .bottomLeading)
}
.frame(width: 600, height: 400)
.gesture(magni)
}
}
This is ScrollView, I want it can appear and disappear. When MagnificationGesture is changing, scrollview can keep somewhere.
struct Scroll:View {
#Binding var current:CGFloat
let intRandom = Int.random(in: 1..<18)
var body: some View {
ScrollViewReader { proxy in
HStack {
Button("Foreword"){
proxy.scrollTo(9, anchor: .center)
}
}
ScrollView(.horizontal) {
HStack {
ForEach(0..<20) { item in
RoundedRectangle(cornerRadius: 25.0)
.frame(width: 100, height: 40)
.overlay(Text("\(item)").foregroundColor(.white))
.id(item)
}
}
.onChange(of: current, perform: { value in
proxy.scrollTo(13, anchor: .center)
})
}
}
}
}

How to drag across Views in a LazyVGrid with GeometryReader?

I want to drag across rectangles in a grid and change their color. This code is almost working, but this is not always the right rectangle that reacts: it behaves rather randomly. Any hints?
import SwiftUI
struct ContentView: View {
let data = (0...3)
#State private var colors: [Color] = Array(repeating: Color.gray, count: 4)
#State private var rect = [CGRect]()
var columns: [GridItem] =
Array(repeating: .init(.fixed(70), spacing: 1), count: 2)
var body: some View {
LazyVGrid(columns: columns, spacing: 1) {
ForEach(data, id: \.self) { item in
Rectangle()
.fill(colors[item])
.frame(width: 70, height: 70)
.overlay(
GeometryReader{ geo in
Color.clear
.onAppear {
rect.insert(geo.frame(in: .global), at: rect.endIndex)
}
}
)
}
}
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged({ (value) in
if let match = rect.firstIndex(where: { $0.contains(value.location) }) {
colors[match] = Color.red
}
})
)
}
}
If I correctly understood your goal, here is fixed variant (with small modifications to avoid repeated hardcoding).
Tested with Xcode 12.1 / iOS 14.1
struct ContentView: View {
let data = (0...3)
#State private var colors: [Color]
#State private var rect: [CGRect]
init() {
_colors = State(initialValue: Array(repeating: .gray, count: data.count))
_rect = State(initialValue: Array(repeating: .zero, count: data.count))
}
var columns: [GridItem] =
Array(repeating: .init(.fixed(70), spacing: 1), count: 2)
var body: some View {
LazyVGrid(columns: columns, spacing: 1) {
ForEach(data, id: \.self) { item in
Rectangle()
.fill(colors[item])
.frame(width: 70, height: 70)
.overlay(
GeometryReader{ geo in
Color.clear
.onAppear {
rect[item] = geo.frame(in: .global)
}
}
)
}
}
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged({ (value) in
if let match = rect.firstIndex(where: { $0.contains(value.location) }) {
colors[match] = Color.red
}
})
)
}
}

How to create Radiobuttons in SwiftUI?

I would like to react on a choice of a user. Something similar to this example:
In a 2nd stage would I like to show additional content below each radiobutton, e.g. moving the buttons 2 and 3 from each other in order to give a list of websites for allowing.
So far I haven't found how to do this in SwiftUI.
Many thanks in advance!
Picker(selection: $order.avocadoStyle, label: Text("Avocado:")) {
Text("Sliced").tag(AvocadoStyle.sliced)
Text("Mashed").tag(AvocadoStyle.mashed)
}.pickerStyle(RadioGroupPickerStyle())
This is the code from the 2019 swiftUI essentials keynote (SwiftUI Essentials - WWDC 2019. Around 43 minutes in the video they show this example.
It will look like this:
check this out...an easy to use SwiftUI RadiobuttonGroup for iOS
you can use it like this:
RadioButtonGroup(items: ["Rome", "London", "Paris", "Berlin", "New York"], selectedId: "London") { selected in
print("Selected is: \(selected)")
}
and here is the code:
struct ColorInvert: ViewModifier {
#Environment(\.colorScheme) var colorScheme
func body(content: Content) -> some View {
Group {
if colorScheme == .dark {
content.colorInvert()
} else {
content
}
}
}
}
struct RadioButton: View {
#Environment(\.colorScheme) var colorScheme
let id: String
let callback: (String)->()
let selectedID : String
let size: CGFloat
let color: Color
let textSize: CGFloat
init(
_ id: String,
callback: #escaping (String)->(),
selectedID: String,
size: CGFloat = 20,
color: Color = Color.primary,
textSize: CGFloat = 14
) {
self.id = id
self.size = size
self.color = color
self.textSize = textSize
self.selectedID = selectedID
self.callback = callback
}
var body: some View {
Button(action:{
self.callback(self.id)
}) {
HStack(alignment: .center, spacing: 10) {
Image(systemName: self.selectedID == self.id ? "largecircle.fill.circle" : "circle")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: self.size, height: self.size)
.modifier(ColorInvert())
Text(id)
.font(Font.system(size: textSize))
Spacer()
}.foregroundColor(self.color)
}
.foregroundColor(self.color)
}
}
struct RadioButtonGroup: View {
let items : [String]
#State var selectedId: String = ""
let callback: (String) -> ()
var body: some View {
VStack {
ForEach(0..<items.count) { index in
RadioButton(self.items[index], callback: self.radioGroupCallback, selectedID: self.selectedId)
}
}
}
func radioGroupCallback(id: String) {
selectedId = id
callback(id)
}
}
struct ContentView: View {
var body: some View {
HStack {
Text("Example")
.font(Font.headline)
.padding()
RadioButtonGroup(items: ["Rome", "London", "Paris", "Berlin", "New York"], selectedId: "London") { selected in
print("Selected is: \(selected)")
}
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ContentViewDark_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environment(\.colorScheme, .dark)
.darkModeFix()
}
}
I just edited #LizJ answer , by adding Binding instead of didTapActive & didTapInactive , so like that it will looks like other SwiftUI elements
import SwiftUI
struct RadioButton: View {
#Binding var checked: Bool //the variable that determines if its checked
var body: some View {
Group{
if checked {
ZStack{
Circle()
.fill(Color.blue)
.frame(width: 20, height: 20)
Circle()
.fill(Color.white)
.frame(width: 8, height: 8)
}.onTapGesture {self.checked = false}
} else {
Circle()
.fill(Color.white)
.frame(width: 20, height: 20)
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
.onTapGesture {self.checked = true}
}
}
}
}
I'm using swift4, Catalina OS and Xcode 11.2 and was having the issue where RadioGroupPickerStyle was unavailable for iOS and .radiogroup just didn't work (it froze in build) so I made my own that's reusable for other occasions. (notice its only the button so you have to handle the logic yourself.) Hope it helps!
import SwiftUI
struct RadioButton: View {
let ifVariable: Bool //the variable that determines if its checked
let onTapToActive: ()-> Void//action when taped to activate
let onTapToInactive: ()-> Void //action when taped to inactivate
var body: some View {
Group{
if ifVariable {
ZStack{
Circle()
.fill(Color.blue)
.frame(width: 20, height: 20)
Circle()
.fill(Color.white)
.frame(width: 8, height: 8)
}.onTapGesture {self.onTapToInactive()}
} else {
Circle()
.fill(Color.white)
.frame(width: 20, height: 20)
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
.onTapGesture {self.onTapToActive()}
}
}
}
}
TO USE: Put this in any file and you can use it as you would any other view anywhere else in the project. (we keep a global folder that has a buttons file in it)
I will use the previous answer of #LizJ and i will add a text after the radio button to resemble (RadioListTile in Flutter)
struct RadioButton: View {
let ifVariable: Bool //the variable that determines if its checked
let radioTitle: String
var onTapToActive: ()-> Void//action when taped to activate
let onTapToInactive: ()-> Void //action when taped to inactivate
var body: some View {
Group{
if ifVariable {
HStack(alignment: .center, spacing: 16) {
ZStack{
Circle()
.fill(AppColors.primaryColor)
.frame(width: 20, height: 20)
Circle()
.fill(Color.white)
.frame(width: 8, height: 8)
}.onTapGesture {self.onTapToInactive()}
Text(radioTitle)
.font(.headline)
}
} else {
HStack(alignment: .center, spacing: 16){
Circle()
.fill(Color.white)
.frame(width: 20, height: 20)
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
.onTapGesture {self.onTapToActive()}
Text(radioTitle)
.font(.headline)
}
}
}
}
I will also provide an example for the selection logic
we will create a enum for radio cases
enum PaymentMethod: Int {
case undefined = 0
case credit = 1
case cash = 2
}
then we will create #State variable to carry the selection, i will not recreate another SwiftUI view but only explain the basic concept without any boilerplate code
struct YourView: View {
#State private var paymentMethod: PaymentMethod
var body: some View {
RadioButton(ifVariable: paymentMethod == PaymentMethod.credit,radioTitle: "Pay in Credit", onTapToActive: {
paymentMethod = .credit
}, onTapToInactive: {})
RadioButton(ifVariable: paymentMethod == PaymentMethod.cash,radioTitle: "Pay in Cash", onTapToActive: {
paymentMethod = .cash
}, onTapToInactive: {})
}
}
with this previous code you can toggle between radio buttons in SwiftUI with a text after each selection to resemble (RadioListTile in Flutter)