SwiftUI drag gesture across multiple subviews - ios

I'm attempting to create a grid of small square views, that when the user hovers over them with their thumb (or swipes across them), the little squares will temporarily "pop up" and shake. Then, if they continue to long press on that view, it would open up another view with more information.
I thought that implementing a drag gesture on the square views would be enough, but it looks like only one view can capture a drag gesture at a time.
Is there way to enable multiple views to capture a drag gesture, or a way to implement a "hover" gesture for iOS?
Here is my main Grid view:
import SwiftUI
struct ContentView: View {
#EnvironmentObject var data: PlayerData
var body: some View {
VStack {
HStack {
PlayerView(player: self.data.players[0])
PlayerView(player: self.data.players[1])
PlayerView(player: self.data.players[2])
}
HStack {
PlayerView(player: self.data.players[3])
PlayerView(player: self.data.players[4])
PlayerView(player: self.data.players[5])
}
HStack {
PlayerView(player: self.data.players[6])
PlayerView(player: self.data.players[7])
PlayerView(player: self.data.players[8])
}
HStack {
PlayerView(player: self.data.players[9])
PlayerView(player: self.data.players[10])
}
}
}
}
And here is my Square view that would hold a small summary to display on the square:
import SwiftUI
struct PlayerView: View {
#State var scaleFactor: CGFloat = 1.0
var player: Player = Player(name: "Phile", color: .green, age: 42)
var body: some View {
ZStack(alignment: .topLeading) {
Rectangle().frame(width: 100, height: 100).foregroundColor(player.color).cornerRadius(15.0).scaleEffect(self.scaleFactor)
VStack {
Text(player.name)
Text("Age: \(player.age)")
}.padding([.top, .leading], 10)
}.gesture(DragGesture().onChanged { _ in
self.scaleFactor = 1.5
}.onEnded {_ in
self.scaleFactor = 1.0
})
}
}

Here is a demo of possible approach... (it is simplified version of your app data settings, but the idea and direction where to evolve should be clear)
The main idea that you capture drag not in item view but in the content view transferring needed states (or calculable dependent data) into item view when (or if) needed.
struct PlayerView: View {
var scaled: Bool = false
var player: Player = Player(name: "Phile", color: .green, age: 42)
var body: some View {
ZStack(alignment: .topLeading) {
Rectangle().frame(width: 100, height: 100).foregroundColor(player.color).cornerRadius(15.0).scaleEffect(scaled ? 1.5 : 1)
VStack {
Text(player.name)
Text("Age: \(player.age)")
}.padding([.top, .leading], 10)
}.zIndex(scaled ? 2 : 1)
}
}
struct ContentView: View {
#EnvironmentObject var data: PlayerData
#GestureState private var location: CGPoint = .zero
#State private var highlighted: Int? = nil
private var Content: some View {
VStack {
HStack {
ForEach(0..<3) { i in
PlayerView(scaled: self.highlighted == i, player: self.data.players[i])
.background(self.rectReader(index: i))
}
}
.zIndex((0..<3).contains(highlighted ?? -1) ? 2 : 1)
HStack {
ForEach(3..<6) { i in
PlayerView(scaled: self.highlighted == i, player: self.data.players[i])
.background(self.rectReader(index: i))
}
}
.zIndex((3..<6).contains(highlighted ?? -1) ? 2 : 1)
}
}
func rectReader(index: Int) -> some View {
return GeometryReader { (geometry) -> AnyView in
if geometry.frame(in: .global).contains(self.location) {
DispatchQueue.main.async {
self.highlighted = index
}
}
return AnyView(Rectangle().fill(Color.clear))
}
}
var body: some View {
Content
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.updating($location) { (value, state, transaction) in
state = value.location
}.onEnded {_ in
self.highlighted = nil
})
}
}

Related

Updating tapGesture area(frame) after device is rotated SwiftUI

I have an issue with updating the area(frame) of .onTapGesture after a device is rotated. Basically, even after changing #State var orientation the area where .onTapGesture works remain the same as on the previous orientation.
Would appreciate having any advice on how to reset that tap gesture to the new area after rotation.
Thanks in advance!
struct ContentView: View {
var viewModel = SettingsSideMenuViewModel()
var body: some View {
VStack {
SideMenu(viewModel: viewModel)
Button("Present menu") {
viewModel.isShown.toggle()
}
Spacer()
}
.padding()
}
}
final class SettingsSideMenuViewModel: ObservableObject {
#Published var isShown = false
func dismissHostingController() {
guard !isShown else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
debugPrint("viewShoudBeDismissedHere")
}
}
}
struct SideMenu: View {
#ObservedObject var viewModel: SettingsSideMenuViewModel
#State private var orientation = UIDeviceOrientation.unknown
var sideBarWidth = UIScreen.main.bounds.size.width * 0.7
var body: some View {
GeometryReader { proxy in
ZStack {
GeometryReader { _ in
EmptyView()
}
.background(Color.black.opacity(0.6))
.opacity(viewModel.isShown ? 1 : 0)
.animation(.easeInOut.delay(0.2), value: viewModel.isShown)
.onTapGesture {
viewModel.isShown.toggle()
viewModel.dismissHostingController()
}
content
}
.edgesIgnoringSafeArea(.all)
.frame(width: proxy.size.width,
height: proxy.size.height)
.onRotate { newOrientation in
orientation = newOrientation
}
}
}
var content: some View {
HStack(alignment: .top) {
ZStack(alignment: .top) {
Color.white
Text("SOME VIEW HERE")
VStack(alignment: .leading, spacing: 20) {
Text("SOME VIEW HERE")
Divider()
Text("SOME VIEW HERE")
Divider()
Text("SOME VIEW HERE")
}
.padding(.top, 80)
.padding(.horizontal, 40)
}
.frame(width: sideBarWidth)
.offset(x: viewModel.isShown ? 0 : -sideBarWidth)
.animation(.default, value: viewModel.isShown)
Spacer()
}
}
}
struct DeviceRotationViewModifier: ViewModifier {
let action: (UIDeviceOrientation) -> Void
func body(content: Content) -> some View {
content
.onAppear()
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
action(UIDevice.current.orientation)
}
}
}
extension View {
func onRotate(perform action: #escaping (UIDeviceOrientation) -> Void) -> some View {
self.modifier(DeviceRotationViewModifier(action: action))
}
}
struct SideMenu_Previews: PreviewProvider {
static var viewModel = SettingsSideMenuViewModel()
static var previews: some View {
SideMenu(viewModel: viewModel)
}
}
In this example is just slideoutMenu with a blurred area. By opening that menu in portrait and taping on the blurred area this menu should close. The issue is when the menu is opened in portrait and then rotated to landscape - the tapGesture area stays the same as it was in portrait, hence if tapped in the landscape - nothing happens. This works in the same direction too. Thus the question is how to reset the tapGesture area on rotation?
This view is presented in UIHostingController. slideOutView?.modalPresentationStyle = .custom the issue is there. But if slideOutView?.modalPresentationStyle = .fullScreen (or whatever) - everything works okay.

How to scroll through items in scroll view using keyboard arrows in SwiftUI?

I've built a view that has scroll view of horizontal type with HStack for macOS app. Is there a way to circle those items using keyboard arrows?
(I see that ListView has a default behavior but for other custom view types there are none)
click here to see the screenshot
var body: some View {
VStack {
ScrollView(.horizontal, {
HStack {
ForEach(items.indices, id: \.self) { index in
//custom view for default state and highlighted state
}
}
}
}
}
any help is appreciated :)
You could try this example code, using my previous post approach, but with a horizontal scrollview instead of a list. You will have to adjust the code to your particular app. My approach consists only of a few lines of code that monitors the key events.
import Foundation
import SwiftUI
import AppKit
struct ContentView: View {
let fruits = ["apples", "pears", "bananas", "apricot", "oranges"]
#State var selection: Int = 0
#State var keyMonitor: Any?
var body: some View {
ScrollView(.horizontal) {
HStack(alignment: .center, spacing: 0) {
ForEach(fruits.indices, id: \.self) { index in
VStack {
Image(systemName: "globe")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(10)
Text(fruits[index]).tag(index)
}
.background(selection == index ? Color.red : Color.clear)
.padding(10)
}
}
}
.onAppear {
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { nsevent in
if nsevent.keyCode == 124 { // arrow right
selection = selection < fruits.count ? selection + 1 : 0
} else {
if nsevent.keyCode == 123 { // arrow left
selection = selection > 1 ? selection - 1 : 0
}
}
return nsevent
}
}
.onDisappear {
if keyMonitor != nil {
NSEvent.removeMonitor(keyMonitor!)
keyMonitor = nil
}
}
}
}
Approach I used
Uses keyboard shortcuts on a button
Alternate approach
To use commands (How to detect keyboard events in SwiftUI on macOS?)
Code:
Model
struct Item: Identifiable {
var id: Int
var name: String
}
class Model: ObservableObject {
#Published var items = (0..<100).map { Item(id: $0, name: "Item \($0)")}
}
Content
struct ContentView: View {
#StateObject private var model = Model()
#State private var selectedItemID: Int?
var body: some View {
VStack {
Button("move right") {
moveRight()
}
.keyboardShortcut(KeyEquivalent.rightArrow, modifiers: [])
ScrollView(.horizontal) {
LazyHGrid(rows: [GridItem(.fixed(180))]) {
ForEach(model.items) { item in
ItemCell(
item: item,
isSelected: item.id == selectedItemID
)
.onTapGesture {
selectedItemID = item.id
}
}
}
}
}
}
private func moveRight() {
if let selectedItemID {
if selectedItemID + 1 >= model.items.count {
self.selectedItemID = model.items.last?.id
} else {
self.selectedItemID = selectedItemID + 1
}
} else {
selectedItemID = model.items.first?.id
}
}
}
Cell
struct ItemCell: View {
let item: Item
let isSelected: Bool
var body: some View {
ZStack {
Rectangle()
.foregroundColor(isSelected ? .yellow : .blue)
Text(item.name)
}
}
}

SwiftUI - Animate View expansion (show / hide)

I have a View that contains a HStack and a DatePicker. When you tap on the HStack, the DatePicker is shown / hidden. I want to animate this action like the animation of Starts and Ends row in iOS Calendar's New Event View.
struct TimePicker: View {
#Binding var startTime: Date
#State private var isDatePickerVisible: Bool = false
var body: some View {
VStack(alignment: .center) {
HStack {
ListItemView(icon: "start-time",
leadingText: "Start Time",
trailingText: startTime.stringTime())
}
.onTapGesture {
withAnimation {
self.isDatePickerVisible.toggle()
}
}
Group {
if isDatePickerVisible {
DatePicker("", selection: $startTime, displayedComponents: [.hourAndMinute])
.datePickerStyle(WheelDatePickerStyle())
}
}
.background(Color.red)
.modifier(AnimatingCellHeight(height: isDatePickerVisible ? 300 : 0))
}
}
}
I have used the following code for animation. It almost works. The only problem is that HStack jumps. And I can not fix it.
https://stackoverflow.com/a/60873883/8292178
struct AnimatingCellHeight: AnimatableModifier {
var height: CGFloat = 0
var animatableData: CGFloat {
get { height }
set { height = newValue }
}
func body(content: Content) -> some View {
content.frame(height: height)
}
}
How to fix this issue? How to animate visibility of the DatePicker?
It's simple, you don't need extra ViewModifier
struct TimePicker: View {
#Binding var startTime: Date
#State private var isDatePickerVisible: Bool = false
var body: some View {
VStack(alignment: .center) {
HStack {
ListItemView(icon: "start-time"
, leadingText: "Start Time"
, trailingText: startTime.stringTime())
}.onTapGesture {
isDatePickerVisible.toggle()
}
if isDatePickerVisible {
DatePicker(""
, selection: $model.startTime
, displayedComponents: [.hourAndMinute]
).datePickerStyle(WheelDatePickerStyle())
}
}.animation(.spring())
}
}

Swift UI and ObservableObject Object Data Cleared

I'm a bit beginner in SWIFT and right now I'm facing a problem whit UI. Let me try to explain my problem.
my homeview screen data coming from web service using Observable object and it loads the data first time. But when I tried to open my left side slide menus than homeView webservice/obervable object data is just cleared when open the left slide menu view. Why my observable object data is empty. Let me share my code:
1.------ This is a my main/parentView
struct ContentView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
withAnimation {
self.viewRouter.showSlideOutMenu = false
self.viewRouter.showDepartmentsMenu = false
self.viewRouter.showAccountMenu = false
}
}
}
return GeometryReader { g in
ZStack(alignment: .leading) {
RouteChanger(viewRouter: self._viewRouter)
if self.viewRouter.showSlideOutMenu {
MainMenuView(viewRouter: self._viewRouter)
.frame(width: g.size.width/2)
.transition(.move(edge: .leading))
}
}
.gesture(drag)
}
}
}
2.----- This is my RouteChanger view for navigate to different pages of my views.
struct RouteChanger: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
GeometryReader { g in
VStack {
if self.viewRouter.currentPage == "Home" {
HomeView()
//.modifier(PageSwitchModifier())
}
}
}
}
}
3.... This is my homeView where I am using Observeable Object
struct HomeView: View {
#ObservedObject var homeController = HomeController()
var body: some View {
GeometryReader { g in
ZStack {
Color(UIColor.midTown.blue)
.edgesIgnoringSafeArea(.top)
VStack { //whole body
if self.homeController.homePageData.CODE == "0" {
ImageViewWidget(imageUrl: (self.homeController.homePageData.DATA?.headerList[0].img_url)!)
.frame(minWidth: g.size.width, maxWidth: g.size.width, minHeight: (g.size.width * UIImage(named: "header")!.size.height) / UIImage(named: "header")!.size.width, maxHeight: (g.size.width * UIImage(named: "header")!.size.height) / UIImage(named: "header")!.size.width)
}
else {
Text("Loading...")
.foregroundColor(Color.blue)
.padding()
.frame(width: g.size.width)
}
}
}
}
}
}
The EnvironmentObject is injected for all subviews automatically, so related part of your ContentView should look like below
ZStack(alignment: .leading) {
RouteChanger() // << here
if self.viewRouter.showSlideOutMenu {
MainMenuView() // << here
.frame(width: g.size.width/2)
.transition(.move(edge: .leading))
}

How make animations work inside ScrollView in SwiftUI? Accordion style animation in SwiftUI

I am trying to build a accordion style view with animations using SwiftUI. But Animations are not working properly if I wrap it into a ScrollView.Below is the code that I am trying to work out.
struct ParentView: View {
#State var isPressed = false
var body: some View {
GeometryReader { geometry in
ScrollView {
Group {
SampleView(index: 1)
SampleView(index: 2)
SampleView(index: 3)
SampleView(index: 4)
}.border(Color.black).frame(maxWidth: .infinity)
}.border(Color.green)
}
}
}
struct SampleView: View {
#State var index: Int
#State var isPressed = false
var body: some View {
Group {
HStack(alignment:.top) {
VStack(alignment: .leading) {
Text("********************")
Text("This View = \(index)")
Text("********************")
}
Spacer()
Button("Press") { self.isPressed.toggle() }
}
if isPressed {
VStack(alignment: .leading) {
Text("********************")
Text("-----> = \(index)")
Text("********************")
}.transition(.slide).animation(.linear).border(Color.red)
}
}
}
}
And Below is the screenshot of problem I'm facing.
To make it animate, wrap self.isPressed.toggle() in a withAnimation closure.
Button("Press") { withAnimation { self.isPressed.toggle() } }

Resources