Cannot find type 'Content' in Scope - ios

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)
}
}

Related

Add modifier to all instances of view

I want to use my theme color on all instances of Toggle(), is there a way to do this with an extension?
extension Toggle {
func content() -> some View {
self.tint(.red)
}
}
The above is not working, is there something else I should call on the extension to modify all instances of Toggle?
This is exactly what .toggleStyle is designed for. Create your own custom ToggleStyle:
struct MyToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
// This just extends the default Toggle appearance, but you can return
// any View you like here. It doesn't have to call `Toggle` first.
Toggle(configuration)
.tint(.red) // Along with whatever other styles you like
}
}
extension ToggleStyle where Self == MyToggleStyle {
static var myToggleStyle: MyToggleStyle { .init() }
}
Then in your top-level ContentView, add the modifier:
.toggleStyle(.myToggleStyle)
Your style will be applied to all Toggles inside of your ContentView.
The best way to do this is to make a custom view with #ViewBuilder.
struct CustomToggle<Content: View>: View {
var isOn: Binding<Bool>
var label: Content
var body: some View {
Toggle(isOn: isOn) { label }
.tint(.red)
}
init(isOn: Binding<Bool>, #ViewBuilder label: #escaping () -> Content) {
self.isOn = isOn
self.label = label()
}
}
If you want to create a modifier to apply to an instance of Toggle(), can do that with the help of ViewModifiers.
i.e: First create a ViewModifier:
struct TintColorModifier: ViewModifier {
func body(content: Content) -> some View {
content
.tint(.red)
}
}
extension Toggle {
func tintStyle() -> some View {
modifier(TintColorModifier())
}
}
Now you can use the extension this way:
struct ContentView: View {
var body: some View {
Toggle()
.tintStyle() // <-- Here
}
}

SwiftUI system layout breaks when using custom transition and custom identifier

I have an issue with the SwiftUI system layout on Xcode 12.5.1
On a Text View, I'm using a custom transition(), which is based on custom ViewModifiers. In addition, I'm also using a custom id() that allow me to tell the system that this Text View is not the same, therefore triggering a transition animation.
When using these 2 modifiers on a view, its parent and the content itself doesn't respect the system layout expected behaviour. You can see exactly what happens in this video
Expected behaviour
Actual behaviour
Here is my sample project code that you can test yourself :
import SwiftUI
struct ContentView: View {
private let contentArray: [String] = ["QWEQWE", "ASDASD", "TOTO", "FOO", "BAR"]
#State private var content: String = "content"
var body: some View {
VStack {
VStack {
Text(self.content)
// commenting either the transition modifier, or the id modifier, make the system layout work as expected. The problem comes from using both at the same time
.transition(.customTransition)
.id(self.content)
}
.background(Color.blue)
Button("action") {
guard let newValue = self.contentArray.randomElement() else {
print("Failed to get a random element from property contentArray")
return
}
withAnimation {
self.content = newValue
}
}
}
.background(Color.green)
}
}
extension AnyTransition {
static var customTransition: AnyTransition {
// uncomment the following line make the system layout work as expected. The problem comes from AnyTransition.modifier(active:identity:)
// return AnyTransition.slide.combined(with: .opacity)
let outAnimation = AnyTransition.modifier(active: CustomTrailingOffset(percent: 1), identity: CustomTrailingOffset(percent: 0)).combined(with: .opacity)
let inAnimation = AnyTransition.modifier(active: CustomLeadingOffset(percent: 1), identity: CustomLeadingOffset(percent: 0)).combined(with: .opacity)
return AnyTransition.asymmetric(insertion: inAnimation, removal: outAnimation)
}
}
struct CustomLeadingOffset: ViewModifier {
var percent: CGFloat = 0
func body(content: Content) -> some View {
GeometryReader { geo in
content.offset(CGSize(width: -geo.size.width*self.percent, height: 0.0))
}
}
}
struct CustomTrailingOffset: ViewModifier {
var percent: CGFloat = 0
func body(content: Content) -> some View {
GeometryReader { geo in
content.offset(CGSize(width: geo.size.width*self.percent, height: 0.0))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.device)
.previewDevice("iPhone 12")
}
}
Tested on Xcode 12.5.1
I've found nothing regarding this behaviour during my researches, not on blog posts, not on forums, not on official documentation, that's the reason why I'm posting this question here, in case anyone ever encounter this problem, knows why it occurs, or knows a solution. I've fill a radar (FB9605660) to Apple in parallel but I'm not sure if I'll be informed to their conclusions.

SwiftUI: draw a rectangle around a view when tapped

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
}
}
}

How to check if a view is displayed on the screen? (Swift 5 and SwiftUI)

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.

SwiftUI - Close Keyboard on Scroll

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())
}
}

Resources