I want to implement a SwiftUI button, that accepts onTapGesture and onLongPressGesture, with an overridable handler
I have been unable to get the native button to work with both so have resorted to an HStack. The buttons use the CommandController pattern and dispatches the command from onTapGesture. The basic code was:
struct CmdButton : View {
#EnvironmentObject var appController : AppController
var command : AppCommand
init(command : AppCommand) {
self.command = command
self.onTapGesture {
self.doCommand()
}
}
var body: some View {
HStack {
Image(self.command.icon).resizable().scaledToFit().frame(width: 35)
}
.padding(6)
.cornerRadius(5)
.contentShape(Rectangle())
.animation(.default)
}
func doCommand(){
appController.dispatchCommand(command: self.command)
}
}
The goal was that CmdButton should call doCommand() from onTapGesture by default unless it was overridden by the implementer, thus:
CmdButton(command: SomeCommand)
.onTapGesture {
doSomethingFirst()
instance.doCommand()
}
.onLongPressGesture {
doLongPressAction()
}
I have 2 core issues.
There seems to be no way I can capture the correct self context in the CmdButton implementation to add the default onTapGesture, if I add during init I get the mutating self error. If I add the gesture to the HStack it cannot be overridden and I can't see how to cast some View to a concrete type to assign directly on the var body.
When I override onTapGesture I am unable to get the self context of the instance in the overridden onTapGesture handler to call the doCommand() method on.
I'm aware I could pass around callbacks and attach them in the implementation, but it might pose other issues capturing context and just seems hacky for something so basic and trivial.
EDIT
To clarify, according Apple docs it is valid to:
struct MyView : View {
let img : String = "asset"
let name : String = "Some Name"
var body: some View {
Image(img).onTapGesture {
onTap()
}
}
func onTap(){
print(name)
}
}
Where the correct context is captured and the gesture handler attached to the Image. My question is instead of the gesture being assigned to the inner implementation how can I assign it to the outer View body so it can be overridden.
MyView()
.onTapGesture {
doSomethingElse()
self_instance.onTap()
}
Again, I can call:
MyView().onTap()
How do I get the View instance to call onTap inside the handler?
Related
I have a simple view that is using a class to generate a link for the user to share.
This link is generated asynchronously so is run by using the .task modifier.
class SomeClass : ObservableObject {
func getLinkURL() async -> URL {
try? await Task.sleep(for: .seconds(1))
return URL(string:"https://www.apple.com")!
}
}
struct ContentView: View {
#State var showSheet = false
#State var link : URL?
#StateObject var someClass = SomeClass()
var body: some View {
VStack {
Button ("Show Sheet") {
showSheet.toggle()
}
}
.padding()
.sheet(isPresented: $showSheet) {
if let link = link {
ShareLink(item: link)
} else {
HStack {
ProgressView()
Text("Generating Link")
}
}
}.task {
let link = await someClass.getLinkURL()
print ("I got the link",link)
await MainActor.run {
self.link = link
}
}
}
}
I've simplified my actual code to this example which still displays the same behavior.
The task is properly executed when the view appears, and I see the debug print for the link. But when pressing the button to present the sheet the link is nil.
The workaround I found for this is to move the .task modifier to be inside the sheet, but that doesn't make sense to me nor do I understand why that works.
Is this a bug, or am I missing something?
It's because the sheet's closure is created before it is shown and it has captured the old value of the link which was nil. To make it have the latest value, i.e. have a new closure created that uses the new value, then just add it to the capture list, e.g.
.sheet(isPresented: $showSheet) { [link] in
You can learn more about this problem in this answer to a different question. Also, someone who submitted a bug report on this was told by Apple to use the capture list.
By the way, .task is designed to remove the need for state objects for doing async work tied to view lifecycle. Also, you don't need MainActor.run.
I'm trying to create navigation for my app using Navigation Stack and routing.
My code is functioning and navigating to views, the problem I'm having is that the view is getting called several times from within a switch statement, I have placed the nav stack in the some scene view, then added a simple link, when tapped it goes through the switch statement and picks up the value 3 times and displays the view, I placed a print statement in the switch and it's printed 3 times for my new view value, following on with database calls etc, they are also getting called 3 times.
I'm new to SwiftUI so I'm sure it's user error, any help would be appreciated, thanks.
enum Routing : Hashable {
case AddFlight
case PilotsList
case newview
}
#State var navPath = NavigationPath()
var body: some Scene {
WindowGroup {
NavigationStack (path: $navPath) {
NavigationLink(value: Routing.newview, label: {Text("Go to new view")})
.navigationDestination(for: Routing.self) { route in
switch route {
case .newview:
Text("New View")
let a = print("New view")
case .PilotsList :
PilotsListView()
case .AddFlight:
AddEditFlightView()
}
}
}
}
}
Putting this in an answer because there is a code "fix" for the reprints.
Verified the behavior both in your code and some of my own existing case statements (XCode Version 14.0.1 (14A400)). Additionally the child view's init is called the same number of multiple times, but work in .onAppear() is only called the once. The number of extra calls seems to vary. Have also verified that it happens even when there isn't a case statement but a single possible ChildView.
This makes me think it may have to do with the Layout system negotiating a size for the view. This closure is a #ViewBuilder which we can tell because we can't just put a print statement into it directly. It's being treated as a subview that's negotiating it's size with the parent. Which makes sense in hindsight, but wow, good to know!
This means that items that should only happen once should go in the .onAppear() code of the child view instead of inside of the destination closure which is a #ViewBuilder.
This code is fairly different than yours but that was mostly to check that the effect wasn't some quirk. The key is that it will only do the "onAppear" task once. Note that the perform closure for .onAppear is NOT a ViewBuilder, it is of type () -> Void. It is possible you might prefer .task{} to do an async database call, and that will also just run the once, it looks like.
struct ThreeTimePrintView:View {
#EnvironmentObject var nav:NavigationManager
var body: some View {
NavigationStack (path: $nav.navPath) {
Button("New View") {
nav.navPath.append(Routing.newview)
}.navigationDestination(for: Routing.self) { route in
switch route {
case .newview:
buildNewView().onAppear() { print("New View onAppear") }
case .PilotsList :
Text("Pilot View")
case .AddFlight:
Text("FligtView")
}
}
}
}
func buildNewView() -> some View {
print("New view")
return Text("New View")
}
}
import SwiftUI
enum Routing : Hashable {
case AddFlight
case PilotsList
case newview
}
final class NavigationManager:ObservableObject {
#Published var navPath = NavigationPath()
}
#main
struct ThreePrintCheckerApp: App {
#StateObject var nav = NavigationManager()
var body: some Scene {
WindowGroup {
ThreeTimePrintView().environmentObject(nav)
}
}
}
When I create a view I wish to login the user and when the login is complete, the view will change.
As I am quite new to SwiftUI it seems the way I am trying to this is not working. (I do know it has something to do with SwiftUI structs)
I have a state variable to know if the progressView should animate
#State private var isLoading: Bool = true
I have an escaping closure to login user
init() {
userService.login { (didError, msg) in
}
}
Progress view(haven't tested it out yet)
ProgressView().disabled(isLoading).frame(alignment: .center)
When I try to do:
userService.login { (didError, msg) in
isLoading.toggle() or self.isLoading.toggle()
}
Xcode return: Escaping closure captures mutating 'self' parameter.
Does anyone know how I can make something like this work?
Instead of init call it in .onAppear somewhere inside body, like
var body: some View {
VStack {
// .. some content
}
.onAppear {
userService.login { (didError, msg) in
self.isLoading.toggle()
}
}
}
I have been struggling with this over and over again, so I think I'm missing something. I need to do math, make a setting, assign a value or any of a host of simple operations in reaction to some user action, such as the example shown here, and SwiftUI is wanting a View where I don't need a view. There's got to be a way around the ViewBuilder's rules. I kind of worked around this by creating an unnecessary view and executing the code I need inside the View's init(), but that seems terribly awkward.
import SwiftUI
struct ContentView: View
{
#State var showStuff = false
var body: some View
{
VStack
{
Toggle(isOn: $showStuff)
{
Text("Label")
}
if showStuff
{
UserDefaults.standard.set(true, forKey: "Something")
}
}
}
}
Way 1 (best):
struct ExecuteCode : View {
init( _ codeToExec: () -> () ) {
codeToExec()
}
var body: some View {
return EmptyView()
}
}
usage:
HStack {
ExecuteCode {
print("SomeView1 was re-drawn!")
}
SomeView1()
}
Way 2:
( my first way is better - you're able to write only simple code here )
Code with let _ = works inside of View!
HStack {
let _ = print("SomeView1 was re-drawn!")
SomeView1()
}
Way 3:
( my first way is better - too difficult code structure; But code doings the same )
HStack {
// here is the magic
{ () -> SomeView1() in
// here is code to execute
print("SomeView1 was re-drawn!")
// here is the magic
return SomeView1()
}
}
Views are actually so-called Function Builders, and the contents of the view body are used as arguments to to the buildBlock function, as mentioned by #Asperi.
An alternative solution if you must run code inside this context is using a closure that returns the desired view:
VStack {
// ... some views ...
{ () -> Text in
// ... any code ...
return Text("some view") }()
// ... some views ...
}
In SwiftUI 2.0, there's a new ViewModifier onChange(of:perform:), that allows you to react to changes in values.
But you can create something similar to that with a neat trick (I forgot where I saw it, so unfortunately I can't leave proper attribution), by extending a Binding with onChange method:
extension Binding {
func onChange(perform action: #escaping (Value, Value) -> Void) -> Self {
.init(
get: { self.wrappedValue },
set: { newValue in
let oldValue = self.wrappedValue
DispatchQueue.main.async { action(newValue, oldValue) }
self.wrappedValue = newValue
})
}
}
You can use it like so:
Toggle(isOn: $showStuff.onChange(perform: { (new, old) in
if new {
UserDefaults.standard.set(true, forKey: "Something")
}
}))
You cannot do what you try to do, because actually every view block inside body is a ViewBuidler.buildBlock function arguments. Ie. you are in function arguments space. I hope you would not expect that expression like
foo(Toggle(), if showStuff { ... } )
would work (assuming foo is func foo(args: View...). But this is what you try to do in body.
So expressions in SwiftUI have to be out of ViewBuilder block (with some exceptions which ViewBuilder itself supports for views).
Here is a solution for your case:
SwiftUI 2.0
struct ContentView: View {
#AppStorage("Something") var showStuff = false
var body: some View {
VStack {
Toggle(isOn: $showStuff) {
Text("Label")
}
}
}
}
SwiftUI 1.0
Find in already solved SwiftUI toggle switches
Note: View.body (excluding some action modifiers) is equivalent of UIView.draw(_ rect:)... you don't store UserDefaults in draw(_ rect:), do you?
I've been seeing some strange behavior for preference keys with ScrollView. If I put the onPreferenceChange inside the ScrollView it won't be called, but if I put it outside it does!
I've setup a width preference key as follows:
struct WidthPreferenceKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat(0)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
The following simple view does not print:
struct ContentView: View {
var body: some View {
ScrollView {
Text("Hello")
.preference(key: WidthPreferenceKey.self, value: 20)
.onPreferenceChange(WidthPreferenceKey.self) {
print($0) // Not being called, we're in a scroll view.
}
}
}
}
But this works:
struct ContentView: View {
var body: some View {
ScrollView {
Text("Hello")
.preference(key: WidthPreferenceKey.self, value: 20)
}
.onPreferenceChange(WidthPreferenceKey.self) {
print($0)
}
}
}
I know that I can use the latter approach to fix this, but sometimes I'm inside a child view that does not have access to its parent scroll view but I still want to record a preference key.
Any ideas on how to get onPreferenceChange to get called inside a ScrollView?
Note: I get Bound preference WidthPreferenceKey tried to update multiple times per frame. when I put the function inside the scroll view, which might explain what is going on but I can't figure it out.
Thanks!
I had been trying to figure out this issue for a long time and have found how to deal with it, although the way I used was just one of the workarounds.
Use onAppear to ScrollView with a flag to make its children show up.
...
#State var isShowingContent = false
...
ScrollView {
if isShowingContent {
ContentView()
}
}
.onAppear {
self.isShowingContent = true
}
Or,
Use List instead of it.
It has the scroll feature, and you can customize it with its own functionality and UITableView appearance in terms of UI. the most important is that it works as we expected.
[If you have time to read more]
Let me say my thought about that issue.
I have confirmed that onPreferenceChange isn't called at the bootstrap time of a view put inside a ScrollView. I'm not sure if it is the right behavior or not. But, I assume that it's wrong because ScrollView has to be capable of containing any views even if some of those use PreferenceKey to pass any data among views inside it. If it's the right behavior, it would be quite easy for us to get in trouble when creating our custom views.
Let's get into more detail.
I suppose that ScrollView would work slightly different from the other container views such as List, (H/V)Stack when it comes to set up its child view at the bootstrap time. In other words, ScrollView would try to draw(or lay out) children in its own way. Unfortunately, that way would affect the children's layout mechanism working incorrectly as what we've been seeing. We could guess what happened with the following message on debug view.
TestHPreferenceKey tried to update multiple times per frame.
It might be a piece of evidence to tell us that the update of children has occurred while ScrollView is doing something for its setup. At that moment, it could be guessed that the update to PreferenceKey has been ignored.
That's why I tried to put the placing child views off to onAppear.
I hope that will be useful for someone who's struggling with various issues on SwiftUI.
I think onPreferenceChange in your example is not called because it’s function is profoundly different from preference(key…)
preference(key:..) sets a preference value for the view it is used on.
whereas onPreferenceChange is a function called on a parent view – a view on a higher position in the view tree hierarchy. Its function is to go through all its children and sub-children and collect their preference(key:) values. When it found one it will use the reduce function from the PreferenceKey on this new value and all the already collected values. Once it has all the values collected and reduced them it will execute the onPreference closure on the result.
In your first example this closure is never called because the Text(“Hello”) view has no children which set the preference key value (in fact the view has no children at all). In your second example the Scroll view has a child which sets its preference value (the Text view).
All this does not explain the multiple times per frame error – which is most likely unrelated.
Recent update (24.4.2020):
In a similar case I could induce the call of onPreferenceChange by changing the Equatable condition for the PreferenceData. PreferenceData needs to be Equatable (probably to detect a change in them). However, the Anchor type by itself is not equatable any longer. To extract the values enclosed in an Anchor type a GeometryProxy is required. You get a GeometryProxy via a GeometryReader. For not disturbing the design of views by enclosing some of them into a GeometryReader I generated one in the equatable function of the PreferenceData struct:
struct ParagraphSizeData: Equatable {
let paragraphRect: Anchor<CGRect>?
static func == (value1: ParagraphSizeData, value2: ParagraphSizeData) -> Bool {
var theResult : Bool = false
let _ = GeometryReader { geometry in
generateView(geometry:geometry, equality:&theResult)
}
func generateView(geometry: GeometryProxy, equality: inout Bool) -> Rectangle {
let paragraphSize1, paragraphSize2: NSSize
if let anAnchor = value1.paragraphRect { paragraphSize1 = geometry[anAnchor].size }
else {paragraphSize1 = NSZeroSize }
if let anAnchor = value2.paragraphRect { paragraphSize2 = geometry[anAnchor].size }
else {paragraphSize2 = NSZeroSize }
equality = (paragraphSize1 == paragraphSize2)
return Rectangle()
}
return theResult
}
}
With kind regards
It seems like the issue is not necessarily with ScrollView, but with your usage of PreferenceKey. For instance, here is a sample struct in which a PreferenceKey is set according to the width of a Rectangle, and then printed using .onPreferenceChange(), all inside of a ScrollView. As you drag the Slider to change the width, the key is updated and the print closure is executed.
struct ContentView: View {
#State private var width: CGFloat = 100
var body: some View {
VStack {
Slider(value: $width, in: 100...200)
ScrollView(.vertical) {
Rectangle()
.background(WidthPreferenceKeyReader())
.onPreferenceChange(WidthPreferenceKey.self) {
print($0)
}
}
.frame(width: self.width)
}
}
}
struct WidthPreferenceKeyReader: View {
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.clear)
.preference(key: WidthPreferenceKey.self, value: geometry.size.width)
}
}
}
As you noted, the first time the key tries to set, the console prints "Bound preference WidthPreferenceKey tried to update multiple times per frame," but a real value is immediately set afterward, and it continues to update dynamically.
What value are you actually trying to set, and what are you trying to do in .onPreferenceChange()?
I think this is because you implemented reduce() incorrectly.
You can find the details here:
https://stackoverflow.com/a/73300115/4366470
TL;DR: Replace value = nextValue() in reduce() with value += nextValue().
You may only read it in superView, but you can change it with transformPreference after you set it .
struct ContentView: View {
var body: some View {
ScrollView {
VStack{
Text("Hello")
.preference(key: WidthPreferenceKey.self, value: 20)
}.transformPreference(WidthPreferenceKey.self, {
$0 = 30})
}.onPreferenceChange(WidthPreferenceKey.self) {
print($0)
}
}
}
The last value is 30 now. Hope it is what you want.
You can read from other layer:
ScrollView {
Text("Hello").preference(key: WidthPreferenceKey.self, value: CGFloat(40.0))
.backgroundPreferenceValue(WidthPreferenceKey.self) { x -> Color in
print(x)
return Color.clear
}
}
The problem here is actually not in ScrollView but in usage - this mechanism allow to transfer data up in viewTree:
A view with multiple children automatically combines its values for a
given preference into a single value visible to its ancestors.
source
The keywords here - with multiple children. This mean that u can pass it in viewTree from child to parent.
Let's review u'r code:
struct ContentView: View {
var body: some View {
ScrollView {
Text("Hello")
.preference(key: WidthPreferenceKey.self, value: 20)
.onPreferenceChange(WidthPreferenceKey.self) {
print($0) // Not being called, we're in a scroll view.
}
}
}
}
As u can see now - child pass value to itself, and not to parent - so this don't want to work, as per design.
And working case:
struct ContentView: View {
var body: some View {
ScrollView {
Text("Hello")
.preference(key: WidthPreferenceKey.self, value: 20)
}
.onPreferenceChange(WidthPreferenceKey.self) {
print($0)
}
}
}
Here, ScrollView is parent and Text is child, and child talk to parent - everything works as expected.
So, as I sad in the beginning the problem here not in ScrollView but in usage and in Apple documentation (u need to read it few times as always).
And regarding this:
Bound preference WidthPreferenceKey tried to update multiple times per
frame.
This is because u may change multiply values in same time and View can't be rendered, try to .receive(on:) or DispatchQueue.main.async as workaround (I guess this may be a bug)