I have a canvas in which I'm drawing views. I want to be able to reposition each view on drag in the canvas, but any gesture inside an ItemView is not recognised, I was thinking of implementing gestures outside the canvas, but I couldn't find anything about canvas hit-testing to select the item I want to reposition using context.translateBy, I'm not completely sure how to proceed since I couldn't find much about using gestures inside the canvas with multiple views.
Current Code:
struct TestCanvasView: View {
#FetchRequest(fetchRequest: Item.all) var items
var currentItems: [Item] {
items.filter { $0.parent == nil }
}
#State var position = CGPoint.zero
var body: some View {
Canvas { context, size in
for currentItem in currentItems {
let itemView = context.resolveSymbol(id: currentItem.objectID)!
context.draw(itemView, at: currentItem.itemPosition)
context.translateBy(x: position.x, y: position.y)
}
} symbols: {
ForEach(currentItems) { item in
ItemView(item: item)
.tag(item.objectID)
// .gesture() doesn't work here
}
}
.gesture(DragGesture()
.onChanged { value in
position = value.location
})
}
}
Related
Working on a project where view are aligned like this
HeaderView
TabView
Views with each tab
List and other items in each view
Relevant code is
struct Home: View {
#StateObject var manageScrollPosition = ManageScrollPosition()
var body: some View {
GeometryReader { gReader in
ScrollView {
VStack {
EntityHeaderView(bottomViewPopupData: $bottomViewPopupData)
EntityTabView(manageScrollPosition: manageScrollPosition)
.frame(height: gReader.size.height)
}
.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self,
value: -$0.frame(in: .named("scroll")).origin.y)
})
.onPreferenceChange(ViewOffsetKey.self) {
if $0 == 238.0 {
manageScrollPosition.isScrollEnable = false
}
}
}
.coordinateSpace(name: "scroll")
.introspectScrollView { scrollView in
scrollView.bounces = manageScrollPosition.isScrollEnable
scrollView.isScrollEnabled = manageScrollPosition.isScrollEnable
}
}
}
I have to disable the scrolling of main scrollview once tab is reaches to top and enable the scrolling of list from child View.
Right now I'm able to achive this with static height of header View
if $0 == 238.0 {
manageScrollPosition.isScrollEnable = false
}
want to replace this static value with actual height of header or some other solution.
I'm new in SwiftUI, Very thank full for your solutiuon in advance.
I am using this solution from Apseri:
SwiftUI | Using onDrag and onDrop to reorder Items within one single LazyGrid?
with this bug fix:
SwiftUI: `onDrop` overlay not going away in LazyVGrid?
Just for some reference here, the grid snippet looks like this:
struct DemoDragRelocateView: View {
#StateObject private var model = Model()
#State private var isUpdating = false
#State private var dragging: GridData?
var body: some View {
ScrollView {
LazyVGrid(columns: model.columns, spacing: 32) {
ForEach(model.data) { d in
GridItemView(d: d)
.overlay(dragging?.id == d.id && isUpdating ? Color.white.opacity(0.8) : Color.clear)
.onDrag {
self.dragging = d
return NSItemProvider(object: String(d.id) as NSString)
}
.onDrop(of: [UTType.text], delegate: DragRelocateDelegate(item: d, listData: $model.data, current: $dragging, updating: $isUpdating))
}
}.animation(.default, value: model.data)
}
.border(.red)
.onDrop(of: [UTType.text], delegate: DropOutsideDelegate(current: $dragging))
}
}
Problem:
when dropping the cell outside of the view, overlay does not reset.
In this case above the animation show a drop on the navigation bar. Not sure why, but the gif changed the color to black instead of the red, for the scrollview border.
How can detect the drop outside of the view so the overlay can be reset in this case?
I have two screens in a SwiftUI app.
Screen A is displayed after the app launch and contains a button which will update the value for currentWorkoutID on the user model and save that modification to a Firestore database.
Screen B is shown from screen A when currentWorkoutID is not nil on the user model.
The problem that I'm seeing is that when I press the button in screen A, screen B immediately shows up (no animation), then is dismissed with animation and then is shown again with no animation.
I'm unsure what the issue is or how to solve it.
struct TrainingProgramDetails: View{
#EnvironmentObject var workoutCompletionStore: WorkoutCompletionDataStore
var presentWorkoutScreenBinding: Binding<Bool> {
Binding(get: {
return userDataStore.user.profileData.currentWorkoutID != nil
},
set: { value in
//nothing to do here
})
}
var body: some View {
let nextWorkoutID = userDataStore.user.profileData.currentWorkoutID
GeometryReader { screenGeometryProxy in
let imageHeight = screenGeometryProxy.size.height / 3
WorkoutsList(workouts: program.workoutsData) {
...
}
.fullScreenCover(isPresented: presentWorkoutScreenBinding,
onDismiss: {
}, content: {
WorkoutScreenAdaptor(workoutID: nextWorkoutID!,
onShowFeedback: showFeedback)
})
}
.edgesIgnoringSafeArea(.top)
}
struct User: Codable {
var profileData: ProfileData = .init()
...
}
struct ProfileData: Codable {
var currentWorkoutID:String?
...
}
Turns out the problem was on a container to this screen that would cause this screen to be taken out of view and then reloaded.
I am implementing a gradient background for the top navigation bar in SwiftUI using a standard NavigationView. For the gradient I am using a UIGraphicsImageRenderer to create a gradient image which I then assign to the backgroundImage of UINavigationBarAppearance. So far everything is behaving correctly in terms of drawing the gradient and having it render on the top bar.
However, I have not yet been able to make the backgroundImage change dynamically per view. Full code below that should work if you paste it into a playground.
import SwiftUI
struct NavBarGradient: ViewModifier {
init(from: UIColor, to: UIColor) {
let appearance = UINavigationBarAppearance()
appearance.backgroundColor = .clear
let imageRenderer = UIGraphicsImageRenderer(size: .init(width: 1, height: 1))
let gradientImage : UIImage = imageRenderer.image { ctx in
let gradientLayer = CAGradientLayer()
gradientLayer.frame = imageRenderer.format.bounds
gradientLayer.colors = [from.cgColor, to.cgColor]
gradientLayer.locations = [0, 1]
gradientLayer.startPoint = .init(x: 0.0, y: 0.0)
gradientLayer.endPoint = .init(x: 0.5, y: 1.0)
gradientLayer.render(in: ctx.cgContext)
}
appearance.backgroundImage = gradientImage
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().compactAppearance = appearance
UINavigationBar.appearance().scrollEdgeAppearance = appearance
}
func body(content: Content) -> some View {
content
}
}
extension View {
func navBarGradient(from: UIColor = .systemRed, to: UIColor = .systemYellow) -> some View {
modifier(NavBarGradient(from: from, to: to))
}
}
struct SimpleView: View {
var body: some View {
Text("Gradient View").navigationTitle("Gradient Colors")
}
}
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(
"Blue to Cyan",
destination: SimpleView()
.navBarGradient(from: .systemBlue, to: .systemCyan)
)
NavigationLink(
"Green to Mint",
destination: SimpleView()
.navBarGradient(from: .systemGreen, to: .systemMint)
)
NavigationLink(
"Red to Yellow",
destination: SimpleView()
.navBarGradient() // comment me out and previous modifier wins
)
}
.navigationTitle("Main Menu")
.navigationBarTitleDisplayMode(.inline)
}
}
}
Right now the .navBarGradient() view modifier seems to be "last one wins" which makes sense given how the view system works in SwiftUI. So what I am trying to understand is - what is the "SwiftUI way" of updating the appearance dynamically?
I have been able to achieve a gradient background in the top bar by adapting solutions like this one; it works exactly as desired. But it seems that it would be a useful thing to be able to update the top bar background image dynamically for things like server-side images.
Thanks in advance!
This is just me wrapping my head around ViewBuilders and the new SwiftUI paradigm.
I have a "menu" at the top of my screen along with a couple of buttons.
If I do a search (tap the magnifying glass), when I return the index always returns to 0 and the first item is selected/displayed. I want to return to the same index it was at when called away. How do I remember the index and reset it?
Here's the Main Menu:
struct TopLevelMenu: View {
/// Toggle to display the Game State (Map) on button tap
#State private var shouldShowWorldMap = false
var body: some View {
NavigationView {
VStack {
if shouldShowWorldMap {
ZStack {
AnimatedSequence()
.modifier(SystemServices())
} else {
TopLevelView()
}
}
}
}
}
struct TopLevelView: View {
/// Tracks the sheet presentation and current play environment (continent)
/// mapCenter, display flags, gameOver flat, current continent
var gameState: GameState = GameState.shared
/// Handles the game logic, turns, scoring, etc.
var gameManager: GameManager = GameManager.shared
/// current game modes: continent, country, capital, grid, about
#State private var continentIndex = 0
/// If Help or the World Map is not displayed (triggered by buttons at the top), then this is the Main Display after launch
var body: some View {
VStack {
Section {
Picker(selection: $continentIndex, label: Text("N/A")) {
ForEach(0 ..< 5) {
Text(Continent.continents[$0]).tag($0)
}
}
.pickerStyle(SegmentedPickerStyle())
}
SecondLevelView(continentIndex: continentIndex)
.modifier(SystemServices())
}
}
}
Normally I would write UIKit code to save the index and restore it, but I'm unsure where such code would go since ViewBuilders aren't cooperative with inlined code. What is accepted practice?
I decided to go with the #AppStorage property wrapper.
Since the picker options correspond to the raw values of an enum, I added a key to the enum (ContinentType):
static var key = "ContinentIndex"
Then in the View I replaced:
#State private var continentIndex = 0
with:
#AppStorage(ContinentType.key) var selectedContinentIndex = 0
This remembers the last index selected, so I can navigate away to other game modes, but when I return to this view, it remembers which continent I was working with.
Here's the update in context:
Section {
Picker(selection: $selectedContinentIndex, label: Text("Continent")) {
ForEach(0 ..< 5) {
Text(ContinentType.continents[$0]).tag($0)
}
}
.pickerStyle(SegmentedPickerStyle())
}