How can I customize the appearance of PasteButton in SwiftUI? - ios

I have an app that allows the user to paste an image from the pasteboard, if it has an image on it. There is also an option for the user to choose a photo from the library. I had two buttons with a matching style, like this:
struct ButtonView: View {
var body: some View {
VStack {
Button {
// Paste from pasteboard
} label: {
HStack {
Image(systemName: "doc.on.clipboard")
Text("Paste Photo")
}
.font(.title)
}
.padding(.bottom, 8)
Button
{
// Open photo library
} label: {
Image(systemName: "photo.on.rectangle.angled")
Text("Pick a Photo")
}
.font(.title)
}
}
}
I'm trying to adopt the new PasteButton in iOS 16 to avoid the user prompt every time, but I can't find any documentation about how to customize it. In the WWDC 2022 session What's new in privacy, there is a section about the button and the speaker says "customize these buttons to fit with your app's interface," but I can't find any documentation about how to customize or what types of customization are allowed.
I have tried adding a .font(.title) modifier, for example, but that does not affect the text size. Ideally I'd like to change the button text to "Paste Photo" and get rid of the background, in addition to changing the font size, so that it matches how the app used to look with the custom button. How can I achieve this?
struct ButtonView: View {
private let supportedTypes: [UTType] =
[UTType("public.image")!]
var body: some View {
VStack {
PasteButton(supportedContentTypes: supportedTypes) { itemProviders in
// Handle paste
}
.padding(.bottom, 8)
Button
{
// Open photo library
} label: {
Image(systemName: "photo.on.rectangle.angled")
Text("Pick a Photo")
}
.font(.title)
}
}
}

From the Apple Document of PasteButton:-
The system provides a button appearance and label appropriate to the current environment. However, you can use view modifiers like buttonBorderShape(_:), labelStyle(_:), and tint(_:) to customize the button in some contexts.
PasteButton(supportedContentTypes: supportedTypes) { itemProviders in
// Handle paste
}
.labelStyle(.iconOnly) // To show icon only or .titleOnly to show title only
.tint(.red) // To change background color
.buttonBorderShape(.capsule) // To give capsule shape
So you cannot change the title of PasteButton but you can customize the button like above.
Note: You cannot clear the button background using .tint(.clear) this will make the background a little grey and I have also checked it doesn't even support custom LabelStyle.

Related

Bug with NavigationLink and #Binding properties causing unintended Interactions in the app

I've encountered a bug in SwiftUI that could cause unintended interaction with the app without the user's knowledge.
Description
The problem seems to be related to using #Binding properties on the View structs when used in conjunction with NavigationStack and NavigationLink. If you use NavigationView with NavigationLink to display a DetailView that accepts a $Binding parameter, and that parameter is used in some sort of condition in the DetailView, it will result in unexpected behavior.
To clearly show the problem, I'm using a DetailView where the "Blue" or "Red" view is shown depending on the #Binding property. Each of those views has a .onTapGesture() modifier that prints some text when tapped. The problem is that if the Red view is shown, it detects and triggers the action on the Blue view, which could lead to unintended changes in many apps without the user's knowledge.
Replication of the problem
You can easily copy and paste this code into your own file to replicate the bug. To see the unexpected behavior, run the code below and follow these steps on the simulator:
Tap on the DetailView in the NavigationLink.
Tap the blue color area and the console will print "Blue Tapped".
Tap the "RED BUTTON" to switch to the other view.
Tap the red color area and the console will print "Red Tapped".
Now try to tap a blank space below the red area (where the blue area was previously located). The console will print "BLUE tapped" - this is the problem, it seems that the blue view is still active there.
I tested this behavior on: XCode 14.1, iPhone 13 Pro 16.1 iOS Simulator, and on a real iPhone with iOS 16. The result was always the same.
import SwiftUI
struct ContentView: View {
var body: some View {
NavView()
}
}
struct NavView: View {
#State private var colourShowed: Int = 1
var body: some View {
// If the DetailView() was shown directly, (without the NavigationLink and NavigationStack) there would be no such a bug.
// DetailView(colourShowed: $colourShowed)
// The bug is obvious when using the NavigationStack() with the NavigationLink()
NavigationStack {
Form {
NavigationLink(destination: { DetailView(colourShowed: $colourShowed) },
label: { Text("Detail View") })
}
}
}
}
struct DetailView: View {
// It seems like the problem is related to this #Binding property when used in conjunction
// with the NavigationLink in "NavView" View above.
#Binding var colourShowed: Int
var body: some View {
ScrollView {
VStack(spacing: 20){
HStack {
Button("BLUE BUTTON", action: {colourShowed = 1})
Spacer()
Button("RED BUTTON", action: {colourShowed = 2})
}
if colourShowed == 1 {
Color.blue
.frame(height: 500)
// the onTapeGesture() is stillActive here even when the "colourShowed" property is set to '2' so this
// view should therefore be deinitialized.
.onTapGesture {
print("BLUE tapped")
}
// The onAppear() doesn't execute when switching from the Red view to the Blue view.
// It seems like the "Blue" View does not deinitialize itself after being previously shown.
.onAppear(perform: {print("Blue appeared")})
}
else {
Color.red
.frame(height: 100)
.onTapGesture {
print("RED tapped")
}
.onAppear(perform: {print("Red appeared")})
}
}
}
}
}
Is there any solution to prevent this?
This is a common problem encountered by those new to Swift and value semantics, you can fix it by using something called a "capture list" like this:
NavigationLink(destination: { [colourShowed] in
It occurred because DetailView wasn't re-init with the new value of colourShowed when it changed. Nothing in body was using it so SwiftUI's dependency tracking didn't think body had to be recomputed. But since you rely on DetailView being init with a new value you have to add it to the capture list to force body to be recomputed and init a new DetailView.
Here are other questions about the same problem with .sheet and .task.

What is wrong with the iOS Keyboard toolbar in SwiftUI

I am aware I asked a similar question before, but it seems like I have not understood the core concept of how to present a custom toolbar above a keyboard.
I successfully solved my problem on how to present one with a search field (SwiftUI 2.0: Custom keyboard elements).
Now I want to present a keyboard when a textfield within a detail view of a list is clicked, but again the keyboard toolbar does not show. Does anyone have an idea why?
VStack {
Text("Weight:")
TextField("0", text: $weight)
.keyboardType(.decimalPad)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
HStack {
Button(action: {
print("Set bodyweight")
},
label: {Text("Bodyweight")
})
Picker("", selection: $weightType) {
ForEach(weightSuffix.allCases, id: \.self) {
Text($0.rawValue)
}
}
.pickerStyle(.segmented)
}
}
}
}
.border(Color.red)
[EDIT]
After AsperiĀ“s comment I Created a small git: https://gist.github.com/joni8a/bc021ef597cb6efa1ab0ca277d602478
Now it gets even weirder, if I attach the toolbar modifier to the list element I get the intended behavior, showing 1 button above the toolbar
If I append the toolbar modifier to the textfield inside the detail view I get the following result:
I think this is a weird behavior. It seems like I have not understood a core concept of SwiftUI. On the other hand if I can't attach the viewmodifer to the textfield itself, it is hard to uncouple the detail view from the list view ...

Swipe action image color SwiftUI

I'm trying to modify the color of the Image in the swipe action of a list in SwiftUI.
I have tried a few things:
Setting the image with rendering template and after that applied the foregroundColor modifier
Setting foregroundColor modifier directly to button
Tried to set a custom image
I searched a lot for this, and I didn't found anything, not only didn't found solution, I didn't found a NO, like "You can't do that", so this question is for me and anyone in the future trying to do the same thing and they being able to found a quick solution or a quick "Thats impossible".
A piece of my code, really simple:
List {
if viewModel.showSkeleton {
ForEach(0...4, id: \.self) { _ in
SkeletonTransactionRowView().listRowSeparator(.hidden)
}
} else {
ForEach(viewModel.transactions) { transaction in
TransactionRowView(transaction: transaction)
.swipeActions {
Button { viewModel.delete(transaction: transaction) } label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
}
}
.listRowSeparator(.hidden)
}
}
.listStyle(PlainListStyle())
NOTE: I know i can make a custom drag gesture and do all this stuff manually, but I want to know if it's possible to do it with the swipe action modifier.

Context Menu & Haptic touch in SwiftUI

I'm working on a new app using SwiftUI and I need some help in the context menu.
I want to know how I can add a custom preview for the context menu in SwiftUI?
& how I can group menu items in multiple groups & add children for any item in the menu?
also how I can make the delete button in red color? or change the colors for them?
another thing, how I can add a menu on the app icon to open a specific View or make an action like this:
In order to add a custom preview, you can use this https://developer.apple.com/documentation/swiftui/view/contextmenu(menuitems:preview:)
The preview should be something that conforms to View.
To split the items in multiple groups, just add a Divider() between the items.
In order to change the color to red for a Delete item, change the button role to .destructive as in the example below.
To add children to one item, use a Menu as below, but I don't think this approach is encouraged.
Here is an example that includes all the above.
.contextMenu {
Menu("This is a menu") {
Button {
doSomething()
} label: {
Text("Do something")
}
}
Button {
doSomethingAgain()
} label: {
Text("Something")
}
Divider()
Button(role: .destructive) {
performDelete()
} label: {
Label("Delete", systemImage: "trash")
}
} preview: {
Text("This is the preview") // you can add anything that conforms to View here
}

How to disable SwiftUI's default behavior?

SwiftUI makes it very easy to build declarative UIs. However, sometimes they assume defaults that are not necessarily what we want.
Example:
When adding two buttons inside a list row, SwiftUI automatically makes the whole row touchable, and both button's actions are called on row tap. This is the default behavior, as demonstrated in their WWDC videos.
But I do not want this behavior. I want both buttons to work properly, and the row to not be tappable.
Question:
How can we tell our Guacamole expert (to use the WWDC reference) to stop assuming how I want my list (or any other behavior) to work?
Any help would be appreciated.
If the List's default behavior is not required, you could use a VStack:
struct ContentView: View {
var body: some View {
VStack {
Button(action: {
print("foo")
}) {
Image(systemName: "photo")
}
Button(action: {
print("bar")
}) {
Image(systemName: "photo")
}
}
}
}
However if List is really required, then it could be customized by writing a custom ListStyle.
(Also take a look at this question: How to change ListStyle in List.)
It seems that SomethingStyle protocol is the way Apple wants developers to use to modify native SwiftUI elements/behavior. Another example would be ButtonStyle or TextFieldStyle, etc.
I am under the impression that Apple wants to enforce their style guidelines. Also to add onto #backslash-f answer you could also just use a for each instead of a list, this will give you a similar effect and allow much more customization.
struct doubleList: View {
var body: some View {
VStack{
ForEach(1 ..< 10) {index in
Button(action: {
print("foo")
}) {
Image(systemName: "photo")
}
}
}
}
}
Another option to try would be to wrap an UITableView into an UIViewRepresentable and try to enable buttons that way
It seems there might be another way around this by using tap gestures
Image(systemName: "photo")
.gesture(TapGesture().onEnded() {
print("action2")
})

Resources