I'm trying to "select" and "deselect" a view in SwiftUI.
What I thought I could do is, when the user taps on a Text that contains a certain string (in my case it is an emoji) the Text should be surrounded by a rectangle. If the view is being deselected, then the rectangle would disappear.
I have tried the following but it does not work, as nothing changes in the view:
Text(emoji.text)
.onDrag { NSItemProvider(object: emoji.text as NSString) }
.modifier(SelectionSquare(select: selectedEmojis.toggleMatching(element: emoji) ? true: false))
// Getting a warning in the line above: Modifying state during view update, this will cause undefined behavior.
struct SelectionSquare: ViewModifier {
var select: Bool
func body(content: Content) -> some View {
content
.onTapGesture {
content.overlay(Rectangle().border(Color.red, width: 2.0))
// Getting a warning here: Result of call to 'overlay(_:alignment:)' is unused
}
}
}
Why doesn't this work? Is there any other way to do this?
Thanks in advance!
You are getting warning this Result of call to 'overlay(_:alignment:)' is unused because of onTapGesture is not returning anything and overlay return view.
You can use this approach.
struct EmojiView: View {
private var text = "ππππππππππππ"
#State private var selectedEmojis: Bool = false
var body: some View {
Text(text)
.onDrag { NSItemProvider(object: text as NSString) }
.modifier(SelectionSquare(select: $selectedEmojis))
}
}
struct SelectionSquare: ViewModifier {
#Binding var select: Bool
func body(content: Content) -> some View {
self.setBorder(to: content)
.onTapGesture {
select.toggle()
}
}
#ViewBuilder
private func setBorder(to content: Content) -> some View{
if select {
content
.overlay(Rectangle().fill(Color.clear).border(Color.red, width: 2.0))
} else {
content
}
}
}
Related
I'm working on an XCode project and can't figure out how to get rid of this error. I have imported the SwiftUI library which 'Content' is from but XCode doesn't seem to realize that. Any suggestions?
func body(content: Content) -> some View {
content
.blur(radius: spotify.isAuthorized ? 0 : 3)
.overlay(
ZStack {
if !spotify.isAuthorized || Self.debugAlwaysShowing {
Color.black.opacity(0.25)
.edgesIgnoringSafeArea(.all)
if self.finishedViewLoadDelay || Self.debugAlwaysShowing {
loginView
}
}
}
)
.onAppear {
// After the app first launches, add a short delay before
// showing this view so that the animation can be seen.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation(LoginViewController.animation) {
self.finishedViewLoadDelay = true
}
}
}
}
You are using Content without telling the compiler enough detail about it. In ViewModifier and AnimatableModifier, Content is already defined for you as an associatedType by the SwiftUI library.
If you are inside a View or a custom struct type of your own making, Content won't be defined, but you can let the compiler know it's a View by doing something like this:
struct MyStruct<Content> where Content : View {
func body(content: Content) -> some View {
return content.background(Color.black)
}
}
If it's a ViewModifier, like I described, the compiler already knows the constraint and you'll see no error:
struct ExampleStruct : ViewModifier {
func body(content: Content) -> some View {
return content.background(Color.black)
}
}
struct ExampleStruct2 : AnimatableModifier {
func body(content: Content) -> some View {
return content.background(Color.black)
}
}
Since the new Swift version, it looks like the keyboard leaves a blank space when I save the data.
MyView
import SwiftUI
struct AddUpdateUrl: View {
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
#EnvironmentObject var controller: PlacesViewController
#Binding var place: Place
#State var url = ""
var updateMode = false
var body: some View {
VStack {
Text("Website")
.font(.callout)
TextField("", text: $url)
.keyboardType(.URL)
.border(/*#START_MENU_TOKEN#*/Color.black/*#END_MENU_TOKEN#*/, width: 1)
}
.navigationBarItems(
trailing: Button(action: {save()}) {Text("Save")}.disabled(url.isEmpty)
)
.resignKeyboardOnDragGesture()
}
private func save() {
if !url.isEmpty {
place.url = url
if updateMode {
controller.update(place: place)
}
}
presentationMode.wrappedValue.dismiss()
}
}
And the result.
The view before trying to update the website
The view to add the website
After clicking the save button
I have the same behavior on the simulator and on my iPhone 11 (ios 14.3)
The .resignKeyboardOnDragGesture() is an extension that removes the keyboard.
struct ResignKeyboardOnDragGesture: ViewModifier {
var gesture = DragGesture().onChanged {_ in
UIApplication.shared.endEditing(true)
}
func body(content: Content) -> some View {
content.gesture(gesture)
}
}
extension View {
func resignKeyboardOnDragGesture() -> some View {
return modifier(ResignKeyboardOnDragGesture())
}
}
I know this is very late but if you still have this issues.
Add the following to your view:
.ignoresSafeArea(.keyboard)
This should no longer happen.
I have an image view, based on a boolean value i should use tint, if condition is true i should use tint else I should use the original image, I tried in many way using ViewModifier but I can't find the solution. Is that possible to get the expected result using ViewModifier?
struct ViewColor: ViewModifier {
var tint: Bool
func body(content: Content) -> some View {
Group {
if self.tint {
content.foregroundColor(Color("colorText"))
}else
{
// content.foregroundColor(Color("colorAccent"))
}
}
}
}
Here is a solution (tested with Xcode 12b)
struct ViewColor: ViewModifier {
var tint: Bool
#ViewBuilder
func body(content: Content) -> some View {
if self.tint {
content.foregroundColor(Color("colorText"))
} else {
content
}
}
}
I used image inside the button.
To maintain image original color inside Button we should use renderingMode(.original) to tint the image we should userenderingMode(. template), For my scenario I should use conditional operator to get the exact result, Here is the code
struct ButtonIconView: View {
var icon : String
var tint : Bool = true
var body: some View {
Button(action: {
print("Button action")
}){
Image(icon)
.renderingMode(self.tint ? .template : .original) // To get the original image color we should use .original in rendering mode to tint image we should use .template
.resizable().scaledToFit() .modifier(ViewColor(tint: self.tint)).frame(width: 18, height: 18)
}.frame(width: 54, height:48)
}
}
To add and remove tint color using ViewModifier we should follow the #Asperi answer
struct ViewColor: ViewModifier {
var tint: Bool
#ViewBuilder
func body(content: Content) -> some View {
if self.tint {
content.foregroundColor(Color("colorText"))
} else {
content
}
}
}
I have a view like below. I want to find out if it is the view which is displayed on the screen. Is there a function to achieve this?
struct TestView: View {
var body: some View {
Text("Test View")
}
}
You could use onAppear on any kind of view that conforms to View protocol.
struct TestView: View {
#State var isViewDisplayed = false
var body: some View {
Text("Test View")
.onAppear {
self.isViewDisplayed = true
}
.onDisappear {
self.isViewDisplayed = false
}
}
func someFunction() {
if isViewDisplayed {
print("View is displayed.")
} else {
print("View is not displayed.")
}
}
}
PS: Although this solution covers most cases, it has many edge cases that has not been covered. I'll be updating this answer when Apple releases a better solution for this requirement.
You can check the position of view in global scope using GeometryReader and GeometryProxy.
struct CustomButton: View {
var body: some View {
GeometryReader { geometry in
VStack {
Button(action: {
}) {
Text("Custom Button")
.font(.body)
.fontWeight(.bold)
.foregroundColor(Color.white)
}
.background(Color.blue)
}.navigationBarItems(trailing: self.isButtonHidden(geometry) ?
HStack {
Button(action: {
}) {
Text("Custom Button")
} : nil)
}
}
private func isButtonHidden(_ geometry: GeometryProxy) -> Bool {
// Alternatively, you can also check for geometry.frame(in:.global).origin.y if you know the button height.
if geometry.frame(in: .global).maxY <= 0 {
return true
}
return false
}
As mentioned by Oleg, depending on your use case, a possible issue with onAppear is its action will be performed as soon as the View is in a view hierarchy, regardless of whether the view is potentially visible to the user.
My use case is wanting to lazy load content when a view actually becomes visible. I didn't want to rely on the view being encapsulated in a LazyHStack or similar.
To achieve this I've added an extension onBecomingVisible to View that has the same kind of API as onAppear, but will only call the action when the view intersects the screen's visible bounds.
public extension View {
func onBecomingVisible(perform action: #escaping () -> Void) -> some View {
modifier(BecomingVisible(action: action))
}
}
private struct BecomingVisible: ViewModifier {
#State var action: (() -> Void)?
func body(content: Content) -> some View {
content.overlay {
GeometryReader { proxy in
Color.clear
.preference(
key: VisibleKey.self,
// See discussion!
value: UIScreen.main.bounds.intersects(proxy.frame(in: .global))
)
.onPreferenceChange(VisibleKey.self) { isVisible in
guard isVisible else { return }
action?()
action = nil
}
}
}
}
struct VisibleKey: PreferenceKey {
static var defaultValue: Bool = false
static func reduce(value: inout Bool, nextValue: () -> Bool) { }
}
}
Discussion
I'm not thrilled by using UIScreen.main.bounds in the code! Perhaps a geometry proxy could be used for this instead, or some #Environment value β I've not thought about this yet though.
I have a simple search list:
struct ContentView: View {
#State var text:String = ""
var items = 1...100
var body: some View {
VStack {
List {
TextField("Search", text: $text)
Section{
ForEach(items.filter({"\($0)".contains(text)}),id: \.self){(i) in
Text("option \(i)")
}
}
}
}
}
}
How can I make the keyboard close when scrolling for more than 2 cells/few points?
If you are using a ScrollView (probably also with a List but I haven't confirmed it), you could use the UIScrollView appearance, this will affect all ScrollViews though.
UIScrollView.appearance().keyboardDismissMode = .onDrag
A thorough discussion on how to resign the keyboard with various answers can be found for this question.
One solution to resign the keyboard on a drag gesture in the list is using a method on UIApplication window as shown below. For easier handling I created an extension on UIApplication and view modifier for this extension and finally an extension to View:
extension UIApplication {
func endEditing(_ force: Bool) {
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(force)
}
}
struct ResignKeyboardOnDragGesture: ViewModifier {
var gesture = DragGesture().onChanged{_ in
UIApplication.shared.endEditing(true)
}
func body(content: Content) -> some View {
content.gesture(gesture)
}
}
extension View {
func resignKeyboardOnDragGesture() -> some View {
return modifier(ResignKeyboardOnDragGesture())
}
}
So the final modifier for resigning the keyboard is just one modifier that has to be placed on the list like this:
List {
ForEach(...) {
//...
}
}
.resignKeyboardOnDragGesture()
I have also implemented a pure swiftUI version of a search bar that might be interesting for you. You can find it in this answer.
As for now, since iOS 16 beta we have a new modifier scrollDismissesKeyboard() that allows to do exactly what you need.
In your example it should look like
struct ContentView: View {
#State var text: String = ""
var items = 1...100
var body: some View {
List {
TextField("Search", text: $text)
Section {
ForEach(items.filter({"\($0)".contains(text)}), id: \.self) { (i) in
Text("option \(i)")
}
}
}
.scrollDismissesKeyboard(.interactively) // <<-- Put this line
}
}
The scrollDismissesKeyboard() modifier has a parameter that determine the dismiss rules. Here are the possible values:
.automatic: Dismissing based on the context of the scroll.
.immediately: The keyboard will be dismissed as soon as any scroll happens.
.interactively: The keyboard will move/disappear inline with the userβs gesture.
.never: The keyboard will never dismissed when user is scrolling.
Form {
...
}.gesture(DragGesture().onChanged { _ in
UIApplication.shared.windows.forEach { $0.endEditing(false) }
})
#FocusState wrapper along with .focused() TextField modifier can be useful.
struct ContentView: View {
#FocusState private var focusedSearchField: Bool
#State var text:String = ""
var items = 1...100
var body: some View {
VStack {
List {
TextField("Search", text: $text)
.focused($focusedSearchField)
Section{
ForEach(items.filter({"\($0)".contains(text)}),id: \.self){(i) in
Text("option \(i)")
}
}
} // to also allow swipes on items (theoretically)
.simultaneousGesture(DragGesture().onChanged({ _ in
focusedSearchField = false
}))
.onTapGesture { // dissmis on tap as well
focusedSearchField = false
}
}
}
}
struct EndEditingKeyboardOnDragGesture: ViewModifier {
func body(content: Content) -> some View {
content.highPriorityGesture (
DragGesture().onChanged { _ in
UIApplication.shared.endEditing()
}
)
}
}
extension View {
func endEditingKeyboardOnDragGesture() -> some View {
return modifier(EndEditingKeyboardOnDragGesture())
}
}