SwiftUI onDrop not called? - swiftui

SwiftUI's drop API is only fired under certain conditions?
Try dragging the "Drag Me" block over the others.
For some reason, a TextEditor needs to satisfy 2 conditions before it's "superview" can detect the drop?
have a background
disabled
struct ContentView: View {
#StateObject fileprivate var viewModel = ViewModel()
#State private var s1 = "Nope"
#State private var s2 = "Nope"
#State private var s3 = "Nope"
#State private var s4 = "I can detect"
#State private var s5 = "I can detect"
#State private var s6 = "Drag me"
var body: some View {
VStack(spacing: 10) {
TextEditor (text: $s1) // drop not detected
.disabled(true)
TextEditor (text: $s2) // drop not detected
.opacity(0.5)
.disabled(true)
TextEditor (text: $s3) // drop not detected
.opacity(0.5).background(Color.pink.opacity(0.5))
TextEditor (text: $s4) // drop detected
.background(Color.pink.opacity(0.5))
.disabled(true)
TextEditor (text: $s5) // drop detected
.opacity(0.5).background(Color.pink.opacity(0.5))
.disabled(true)
Text("Yes") // drop detected
.background(Color.pink.opacity(0.5))
TextEditor (text: $s6)
.onDrag { () -> NSItemProvider in
return NSItemProvider(object: "Hello" as NSString)
}
}.onDrop(of: [.plainText], delegate: viewModel)
}
}
fileprivate class ViewModel: NSObject, ObservableObject, DropDelegate {
func dropEntered(info: DropInfo) {
print("Drop entered: \(info)")
}
func dropUpdated(info: DropInfo) -> DropProposal? {
print("Drop updated: \(info)")
return DropProposal(operation: .move)
}
func dropExited(info: DropInfo) {
print("Drop exited: \(info)")
}
func performDrop(info: DropInfo) -> Bool {
print("Perform drop: \(info)")
return true
}
}
Tested on Xcode 12.3, iOS 14.3
Am I missing something or is this a bug?

Related

CHanging #FocusState on searchable doesn't make it focused

When the search becomes active i get a callback with .onChange(of: isFocused
but setting isFocused to false doesn't make the search inactive
struct MainSearchable: ViewModifier {
#State var searchText: String = ""
#FocusState private var isFocused: Bool
func body(content: Content) -> some View {
content
.searchable(text: $searchText, placement: .toolbar)
.searchSuggestions {
...
}
.onChange(of: isFocused, perform: { newValue in
print("FOCUSED")
})
.onSubmit(of: .search) {
...
}
.focused($isFocused)
}
}

SwiftUI GoogleMap Delegate Events Causes Infinite Body View Reloading

Im new to SwiftUI and Im using google maps within my app, I need to track 2 Map events as shown in below code,
Main View :
struct HomeView : View {
#State var mapView = GMSMapView()
#State var locationManager = CLLocationManager()
#State var alert = false
#State var currentLocation = CLLocationCoordinate2D()
#State var isLocationChanged = false
var body: some View{
ZStack{
MapView(mapView: self.$mapView, locationManager: self.$locationManager, alert: self.$alert, currentLocation: self.$currentLocation, isLocationChanged: self.$isLocationChanged)
.equatable()
.ignoresSafeArea(.all)
.onAppear{
self.locationManager.requestWhenInUseAuthorization()
}
if isLocationChanged {
Text("Show Search Progress")
.foregroundColor(.black)
.padding(.vertical,10)
.frame(width: UIScreen.main.bounds.width / 2)
}
}// Show Alert
}
}
MapView :
struct MapView : UIViewRepresentable, Equatable {
#Binding var mapView : GMSMapView
#Binding var locationManager : CLLocationManager
#Binding var alert : Bool
#Binding var currentLocation : CLLocationCoordinate2D
#Binding var isLocationChanged : Bool
static func == (lhs: MapView, rhs: MapView) -> Bool {
return lhs.isLocationChanged == rhs.isLocationChanged && lhs.isLocationChanged != rhs.isLocationChanged
}
func makeUIView(context: Context) -> GMSMapView {
mapView.delegate = context.coordinator
locationManager.delegate = context.coordinator
mapView.isMyLocationEnabled = true
return mapView
}
func updateUIView(_ mapView: GMSMapView, context: Context) {
locationManager.startUpdatingLocation()
mapView.animate(toLocation: self.currentLocation)
mapView.animate(toZoom: 15)
}
func makeCoordinator() -> Coordinator {
return Coordinator(googlemapview: self)
}
class Coordinator : NSObject, GMSMapViewDelegate , CLLocationManagerDelegate {
var parent : MapView
init( googlemapview : MapView) {
self.parent = googlemapview
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
self.parent.currentLocation = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
self.parent.locationManager.stopUpdatingLocation()
}
func mapView(_ mapView: GMSMapView, idleAt position: GMSCameraPosition) {
self.parent.isLocationChanged = false
print("========= Idle")
}
func mapView(_ mapView: GMSMapView, willMove gesture: Bool) {
self.parent.isLocationChanged = true
print("================ Changed")
}
}
}
As shown every time the map view rendered it will fire the didChange events which will make the whole view body to reload even the map its self and this will cause an infinite main view reloading, how can i fix this ?
Update
I tried to use EquatableView to ignore the changes from the Map View but Im still getting the same results and the mapView will be redrawn evert time i scroll it ??!! I need to track when user scroll the map to new position
It might be due to location manager recreated, try with StateObject
struct GoogleMapsView: UIViewRepresentable {
#StateObject private var locationManager = LoccationManager() // << here !!
//...
}

SwiftUI: Unable to update the toggle component

I have a state the decides if we need to do round up or down or nothing:
enum RoundingType: Codable {
case up
case down
}
struct ViewState {
var roundingType: RoundingType? = nil
}
Then in the toggle I simply update this flag:
#MainActor
class ViewModel: ObservableObject {
#Published var state: ViewState
func toggleRoundingType(_ roundingType: RoundingType) {
let oldRoundingType = state.roundingType
// if same rounding type, cancel it
// Otherwise, set it
let isCancel = oldRoundingType == roundingType
if isCancel {
state.roundingType = nil
} else {
state.roundingType = roundingType
}
}
}
This is my View:
struct HomeView: View {
#StateObject var viewModel: ViewModel
var body: some View {
let state = viewModel.state
HStack {
Spacer()
SUITextToggle(label: loc(.roundUp), isOn: state.roundingType == .up) { _ in
viewModel.toggleRoundingType(.up)
}
Spacer()
SUITextToggle(label: loc(.roundDown), isOn: state.roundingType == .down) { _ in
viewModel.toggleRoundingType(.down)
}
Spacer()
}
}
}
This view renders 2 toggles, and user can turn on/off the round up/down toggles.
This is my toggle implementation:
public struct SUITextToggle: View {
#State var isOn: Bool
private var binding: Binding<Bool> {
Binding<Bool> {
return isOn
} set: { newValue in
isOn = newValue
onChange(newValue)
}
}
let label: String
let onChange: (Bool) -> Void
init(label: String, isOn: Bool, onChange: #escaping (Bool) -> Void) {
self.label = label
self.isOn = isOn
self.onChange = onChange
}
public var body: some View {
Toggle(label, isOn: binding)
.toggleStyle(.button)
}
}
Now I have an issue that when I turn on "Up", then turn on "Down", the "Up" button is not automatically turned off. For some reason the "Up" button is not refreshed.
EDIT:
minimal reproducible example:
import SwiftUI
import UIKit
public struct SUITextToggle: View {
#State var isOn: Bool
private var binding: Binding<Bool> {
Binding<Bool> {
return isOn
} set: { newValue in
isOn = newValue
onChange(newValue)
}
}
let label: String
let onChange: (Bool) -> Void
init(label: String, isOn: Bool, onChange: #escaping (Bool) -> Void) {
self.label = label
self.isOn = isOn
self.onChange = onChange
}
public var body: some View {
Toggle(label, isOn: binding)
.toggleStyle(.button)
}
}
struct HomeView: View {
#State var isOn: Bool = true
var body: some View {
// Only one of these 2 toggles should be on
SUITextToggle(label: "Toggle 1", isOn: isOn) { _ in
isOn = !isOn
}
SUITextToggle(label: "Toggle 2", isOn: !isOn) { _ in
isOn = !isOn
}
}
}
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let hostingVC = UIHostingController(rootView: HomeView())
present(hostingVC, animated: true, completion: nil)
}
}
The issue is that there is no two-way communication that is compatible with SwiftUI. You communicate one-way on init and then use the completion handler. That does not tell the other toggle to re-render.
I made some changes to make it more SwiftUI.
import SwiftUI
enum RoundingType: String, Codable, CaseIterable, CustomStringConvertible, Identifiable {
case up
case down
//Set the description here
var description: String{
"round\(rawValue)"
}
//Make the enum Identifiable
var id: String{
rawValue
}
}
//No changes
struct ViewState {
var roundingType: RoundingType? = nil
}
#MainActor
class HomeViewModel: ObservableObject {
//Set a default value. Missing from code
#Published var state: ViewState = .init(roundingType: .up)
//Remove func
}
#available(iOS 15.0, *)
struct ToggleHomeView: View {
#StateObject var viewModel: HomeViewModel = .init()
var body: some View {
HStack {
Spacer()
//Iterate through all the options and provide a toggle for each option
ForEach(RoundingType.allCases){ type in
SUITextToggle(selectedType: $viewModel.state.roundingType, toggleType: type, label: type.description)
Spacer()
}
}
}
}
#available(iOS 15.0, *)
struct ToggleHomeView_Previews: PreviewProvider {
static var previews: some View {
ToggleHomeView()
}
}
#available(iOS 15.0, *)
public struct SUITextToggle: View {
//Bindng is a two-way connection
#Binding var selectedType: RoundingType?
///type that toggle represents
let toggleType: RoundingType
///proxy that uses the selectedType and toggleType to set toggle to on/off if the two variables are the same
private var binding: Binding<Bool> {
Binding<Bool> {
return selectedType == toggleType
} set: { newValue in
if newValue{
selectedType = toggleType
}else{
//if you remove the nil set a default value here
selectedType = nil
}
}
}
let label: String
public var body: some View {
Toggle(label, isOn: binding)
.toggleStyle(.button)
}
}
But you can preserve most of your code if you remove the #State. This wrapper is meant to preserve its value through body's re-rendering.
#available(iOS 15.0, *)
public struct SUITextToggle: View {
var isOn: Bool
private var binding: Binding<Bool> {
Binding<Bool> {
return isOn
} set: { newValue in
onChange(newValue)
}
}
let label: String
let onChange: (Bool) -> Void
init(label: String, isOn: Bool, onChange: #escaping (Bool) -> Void) {
self.label = label
self.isOn = isOn
self.onChange = onChange
}
public var body: some View {
Toggle(label, isOn: binding)
.toggleStyle(.button)
}
}
Another "hack" that is out there is to force Views to recreate by setting the id but this causes unnecessary rendering. Efficiency issues as your app grows will become noticeable
#available(iOS 15.0, *)
struct ToggleHomeView: View {
#StateObject var viewModel: HomeViewModel = .init()
var body: some View {
let state = viewModel.state
HStack {
Spacer()
SUITextToggle(label: "roundUp", isOn: state.roundingType == .up) { _ in
viewModel.toggleRoundingType(.up)
}
Spacer()
SUITextToggle(label: "roundDown", isOn: state.roundingType == .down) { _ in
viewModel.toggleRoundingType(.down)
}
Spacer()
}.id(state.roundingType)
}
}

In SwiftUI how do I refer to a view to perform a property on it?

I have a SearchBar that I've added a dismiss property to. dismiss is used by the cancel button, but also might be used by the parent view when displaying a sheet. How do I define the SearchBar in the parent view to be able to reference the dismiss property?
The relevant parts of the SearchBar look like this:
struct SearchBar: View {
#Binding var text: String
#Binding var isSearching: Bool
let prompt: String
var body: some View {
...
}
var dismiss: Void {
// dismiss the keyboard
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
isSearching = false
text = ""
}
}
I envisioned the parent SearchView to look something like this:
struct SearchView: View {
#State private var isShowingDataView = false
#State private var searchText = ""
#State private var isSearching = false
let prompt = "Search"
#State private var searchBar: View
var body: some View {
VStack(alignment: .leading) {
searchBar = SearchBar(text: $searchText, isSearching: $isSearching, prompt: prompt)
...
Button(action: {
showData(data: data)
}) {
HStack {
...
}
}
}
}
func showData(data: Data) {
dataShowing = data
if isSearching {
searchBar.dismiss
}
isShowingDataView = true
}
}
With the above I get the error:
"Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements"
on the searchBar definition line, and
"Type '()' cannot conform to 'View'"
on the VStack line.
dismiss should be a method on your Searchview, and you should pass a closure to SearchBar for it to call when its cancel button is tapped.
struct SearchBar: View {
#Binding var text: String
let prompt: String
let isSearching: Bool
let cancel: () -> Void
var body: some View {
HStack {
TextField(prompt, text: $text)
Button("Cancel", action: cancel)
}
.disabled(!isSearching)
}
}
struct SearchView: View {
#State private var isShowingDataView = false
#State private var searchText = ""
#State private var isSearching = false
let prompt = "Search"
var body: some View {
VStack(alignment: .leading) {
SearchBar(
text: $searchText,
prompt: prompt,
isSearching: $isSearching,
cancel: self.cancelSearch
)
Button(action: {
showData(data: data)
}) {
HStack {
...
}
}
}
}
private func showData(data: Data) {
dataShowing = data
cancelSearch()
isShowingDataView = true
}
private func cancelSearch() {
guard isSearching else { return }
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
isSearching = false
text = ""
}
}

Custom UITextField wrapped in UIViewRepresentable interfering with ObservableObject in NavigationView

Context
I have created a UIViewRepresentable to wrap a UITextField so that:
it can be set it to become the first responder when the view loads.
the next textfield can be set to become the first responder when enter is pressed
Problem
When used inside a NavigationView, unless the keyboard is dismissed from previous views, the view doesn't observe the value in their ObservedObject.
Question
Why is this happening? What can I do to fix this behaviour?
Screenshots
Keyboard from root view not dismissed:
Keyboard from root view dismissed:
Code
Here is the said UIViewRepresentable
struct SimplifiedFocusableTextField: UIViewRepresentable {
#Binding var text: String
private var isResponder: Binding<Bool>?
private var placeholder: String
private var tag: Int
public init(
_ placeholder: String = "",
text: Binding<String>,
isResponder: Binding<Bool>? = nil,
tag: Int = 0
) {
self._text = text
self.placeholder = placeholder
self.isResponder = isResponder
self.tag = tag
}
func makeUIView(context: UIViewRepresentableContext<SimplifiedFocusableTextField>) -> UITextField {
// create textfield
let textField = UITextField()
// set delegate
textField.delegate = context.coordinator
// configure textfield
textField.placeholder = placeholder
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textField.tag = self.tag
// return
return textField
}
func makeCoordinator() -> SimplifiedFocusableTextField.Coordinator {
return Coordinator(text: $text, isResponder: self.isResponder)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<SimplifiedFocusableTextField>) {
// update text
uiView.text = text
// set first responder ONCE
if self.isResponder?.wrappedValue == true && !uiView.isFirstResponder && !context.coordinator.didBecomeFirstResponder{
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
private var isResponder: Binding<Bool>?
var didBecomeFirstResponder = false
init(text: Binding<String>, isResponder: Binding<Bool>?) {
_text = text
self.isResponder = isResponder
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
func textFieldDidBeginEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.isResponder?.wrappedValue = true
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.isResponder?.wrappedValue = false
}
}
}
}
And to reproduce, here is the contentView:
struct ContentView: View {
var body: some View {
return NavigationView { FieldView(tag: 0) }
}
}
and here's the view with the field and its view model
struct FieldView: View {
#ObservedObject private var viewModel = FieldViewModel()
#State private var focus = false
var tag: Int
var body: some View {
return VStack {
// listen to viewModel's value
Text(viewModel.value)
// text field
SimplifiedFocusableTextField("placeholder", text: self.$viewModel.value, isResponder: $focus, tag: self.tag)
// push to stack
NavigationLink(destination: FieldView(tag: self.tag + 1)) {
Text("Continue")
}
// dummy for tapping to dismiss keyboard
Color.green
}
.onAppear {
self.focus = true
}.dismissKeyboardOnTap()
}
}
public extension View {
func dismissKeyboardOnTap() -> some View {
modifier(DismissKeyboardOnTap())
}
}
public struct DismissKeyboardOnTap: ViewModifier {
public func body(content: Content) -> some View {
return content.gesture(tapGesture)
}
private var tapGesture: some Gesture {
TapGesture().onEnded(endEditing)
}
private func endEditing() {
UIApplication.shared.connectedScenes
.filter {$0.activationState == .foregroundActive}
.map {$0 as? UIWindowScene}
.compactMap({$0})
.first?.windows
.filter {$0.isKeyWindow}
.first?.endEditing(true)
}
}
class FieldViewModel: ObservableObject {
var subscriptions = Set<AnyCancellable>()
// diplays
#Published var value = ""
}
It looks like SwiftUI rendering engine again over-optimized...
Here is fixed part - just make destination unique forcefully using .id. Tested with Xcode 11.4 / iOS 13.4
NavigationLink(destination: FieldView(tag: self.tag + 1).id(UUID())) {
Text("Continue")
}