Moving parts of a SwiftUI List closer together - ios

Is it possible to, somehow, move the text and the system image of a Label to be somewhat closer together?
Label("MyString", systemImage: "wifi")

Yes, it's possible!
You just need to create a custom LabelStyle like this…
struct MyLabelStyle: LabelStyle {
let spacing: CGFloat
func makeBody(configuration: Configuration) -> some View {
HStack(spacing: spacing) {
configuration.icon
configuration.title
}
}
}
extension LabelStyle where Self == MyLabelStyle {
static func mine(spacing: CGFloat) -> MyLabelStyle {
MyLabelStyle(spacing: spacing)
}
}
then use like:
struct ContentView: View {
var body: some View {
VStack {
Label("MyString", systemImage: "wifi")
.labelStyle(.mine(spacing: 0))
Label("MyString", systemImage: "wifi")
.labelStyle(.mine(spacing: 10))
Label("MyString", systemImage: "wifi")
.labelStyle(.mine(spacing: 20))
}
}
}

Related

SwiftUI NavigationLink for iOS 14.5 not working

I had the following code in Xcode 12.4 that worked perfectly
ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: rows, spacing: 0) {
HStack {
if (type == "Quiz") {
NavigationLink(destination: Quiz(id: quiz.id)) {
VStack(alignment: .leading) {
Text("Quiz")
.font(.headline)
.foregroundColor(.white)
.padding(.top, 8)
.padding(.leading)
}
.background(Color.green)
.cornerRadius(12)
.shadow(color: .green, radius: 3, x: 0.0, y: 0.0)
}
} else {
NavigationLink(destination: Survey(id: survey.id)) {
VStack(alignment: .leading) {
Text("Survey")
.font(.headline)
.foregroundColor(.white)
.padding(.top, 8)
.padding(.leading)
}
.background(Color.green)
.cornerRadius(12)
.shadow(color: .green, radius: 3, x: 0.0, y: 0.0)
}
} // End If
if (type == "Quiz") {
NavigationLink(destination: QuizResults(id: quiz.id)) {
VStack(alignment: .leading) {
Text("Quiz Results")
.font(.headline)
.foregroundColor(.white)
.padding(.top, 8)
.padding(.leading)
}
.background(Color.blue)
.cornerRadius(12)
.shadow(color: .blue, radius: 3, x: 0.0, y: 0.0)
}
} else {
NavigationLink(destination: SurveyResults(id: survey.id)) {
VStack(alignment: .leading) {
Text("Survey Results")
.font(.headline)
.foregroundColor(.white)
.padding(.top, 8)
.padding(.leading)
}
.background(Color.blue)
.cornerRadius(12)
.shadow(color: .blue, radius: 3, x: 0.0, y: 0.0)
}
}
}
.padding([.leading, .trailing], 25)
}
.frame(height: 100)
I just updated Xcode to 12.5 and the above does not work any more.
It was working fine in 12.4!?
Now when I click the 'Quiz' element, it starts the transition to the Quiz View which is displays it but immediately closes the view and I'm back in the Detail View!?
Can someone see what I am doing wrong, and why now based on the update to 12.5 this stopped working?
UPDATE
I refined the code to the minimal possible reproducible form. What seems to be happening is that I have two or more NavigationLinks sets.
the first is the set to navigate the user to either the Quiz or Survey which the if statement addresses the user to the correct view to fill in.
Where the issue is in 12.5 is that the second set where the user can click to go see the overall results of the Quiz or Survey does not work when it's directly after the first navigation.
Like I said before hand it worked perfectly in 12.4 but seems like 12.5 does not agree with it. Can someone offer a better way for the user to click an element to either go fill in a quiz or survey or go see the results of a quiz or survey?
I got exactly the same problem, everything works fine with Xcode 12.4.
https://developer.apple.com/forums/thread/677333
I try to following this thread, it might work but on some case, I still have this bug.
NavigationLink(destination: EmptyView()) {
EmptyView()
}
Apparently, you can put this 3 lines of code close to your NavigationLink...
If someone got a better answer I will really appreciate it !
What a horrible bug! From my testing and some googling it happens when there are exactly 2 navigation links in a view. The code in the question has 4 but because of the if else statements there are effectively only 2 at a time.
I often don't know how many nav links I will have as it depends on what data the user has added/how many search hits there are etc. To be safe I've made a tripleEmptyNavigationLink modifier which I've stuck at the end of all my views. It's solving the popping behaviour but I'm still getting the 'Unable to present' warnings. Would love to know if anyone has anything better than this!
import SwiftUI
struct TripleEmptyNavigationLink: View {
var body: some View {
VStack {
NavigationLink(destination: EmptyView()) {EmptyView()}
NavigationLink(destination: EmptyView()) {EmptyView()}
NavigationLink(destination: EmptyView()) {EmptyView()}
}
}
}
struct TripleEmptyNavigationLinkBackground: ViewModifier {
func body(content: Content) -> some View {
content
.background(TripleEmptyNavigationLink())
}
}
extension View {
func tripleEmptyNavigationLink()-> some View {
self.modifier(TripleEmptyNavigationLinkBackground())
}
}
usage:
MyView()
.tripleEmptyNavigationLink()
In Xcode13 beta still has this issue.
So far solution:
1、Wrap NavigationLink with List or Form:
List {
NavigationLink(destination: Text("1")) {
Text("1")
}
NavigationLink(destination: Text("2")) {
Text("2")
}
NavigationLink(destination: Text("3")) {
Text("3")
}
}
2、Or Use one NavigationLink, and create destination view from func:
struct TaskIndexPage: View {
func buildView() -> some View {
// return you destination
switch self.option {
case 1:
return Text("\(option)")
default:
return Text("\(option)")
}
}
#State private var showDetail: Bool = false
#State private var option: Int = 0
var body: some View {
VStack {
Button {
showDetail = true
option = 1
} label: { Text("button 1") }
Button {
showDetail = true
option = 2
} label: { Text("button 2") }
Button {
showDetail = true
option = 3
} label: { Text("button 3") }
}
// handle navigating
NavigationLink(destination: self.buildView(), isActive: $showDetail) {}.opacity(0)
}
}
Adding a NavigationLink with an empty view didn't work for me. I solved my issue removing all NavigationLinks from the ForEach and using a single one to control the navigation to the detail view, a tap gesture and 2 state variables to keep track on what is being tapped on.
The example broken code and fix can be found at Paul Hudson's site.
https://www.hackingwithswift.com/forums/swiftui/unable-to-present-please-file-a-bug/7901/8237
Below is the complete working version
import SwiftUI
struct NavigationViewOptions {
enum OptionType { case main, optional }
typealias Option = (id: UUID, value: String, type: Self.OptionType)
static var options: [Option] = [
(UUID(), "Option 1", .main),
(UUID(), "Option 2", .optional),
(UUID(), "Option 3", .main),
(UUID(), "Option 4", .main),
(UUID(), "Option 5", .optional),
]
static func buildView(for option: Option) -> some View {
switch option.type {
case .main:
return Text("Main Option selected\n\(option.value)").font(.title).fontWeight(.bold)
case .optional:
return Text("Optional Option selected\n\(option.value)").font(.title3).italic().fontWeight(.medium)
}
}
}
struct NavigationViewWorking: View {
// State variables to leep track of what option has been tapped on and when to navigate to new view
#State private var selectedOption: NavigationViewOptions.Option = (id:UUID(),"",.main)
#State private var showDetail: Bool = false
var body: some View {
NavigationView {
ScrollView{
VStack (alignment:.leading) {
Text("NAVIGATION FIX FOR:\nUnable to present. Please file a bug.")
.padding(.bottom, 40)
ForEach(NavigationViewOptions.options, id: \.id) { option in
Text(option.value)
.font(.title)
.padding(.vertical, 10)
.foregroundColor(.accentColor) // same color as navigationLink
// handle tap on option
.onTapGesture {
selectedOption = option
showDetail = true
}
}
Spacer()
NavigationLink("", destination: NavigationViewOptions.buildView(for: selectedOption), isActive: $showDetail)
.opacity(0)
}
.navigationTitle("Options")
}
// INITIAL DETAIL VIEW
Text("Select option from the left")
}
}
}
For me the correct answer didn't work.
It showed Unable to present-message and then required view was pushed and poped out back quickly.
While playing around I found a working solution. I keep NotificationLink's without label set as a plain List items.
NavigationView {
ZStack {
List {
NavigationLink(isActive: $isFirstViewPresented,
destination: firstView,
label: EmptyView.init)
NavigationLink(isActive: $isSecondViewPresented,
destination: secondView,
label: EmptyView.init)
}
.listStyle(.plain)
//...
Button("Show first view") { isFirstViewPresented.toggle() }
Button("Show second view") { isSecondViewPresented.toggle() }
}
}
Don't forget to wrap active-properties with #State.
It also has some benefits as for me (all the navigation links are placed at the top of the view-getter and I don't need to look for it through all the code.
I could never find a reliable solution to this horrible bug. So I decided to create a custom NavigationLink, https://gist.github.com/Arutyun2312/a0dab7eecaa84bde99c435fecae76274. This works way better than expected, because all swiftui related functions continue working as usual. Seems like the bug is specifically with NavigationLink.
struct NavigationLink: View {
fileprivate init<T: View>(body: T) {
self.body = .init(body)
}
let body: AnyView
}
private struct NavigationLinkImpl<Destination: View, Label: View>: View {
let destination: () -> Destination?
#State var isActive = false
#ViewBuilder let label: () -> Label
var body: some View {
NavigationLinkImpl1(destination: destination, isActive: $isActive, label: label)
}
}
private struct NavigationLinkImpl1<Destination: View, Label: View>: View {
let destination: () -> Destination
#Binding var isActive: Bool
#ViewBuilder let label: () -> Label
#State var model = Model()
var body: some View {
Button(action: action, label: label)
.introspectNavigationController(customize: handle)
.id(isActive)
}
func handle(nav: UINavigationController) {
if isActive {
if model.destination == nil {
let dest = UIHostingController<Destination>(rootView: destination())
nav.pushViewController(dest, animated: true)
model.destination = dest
}
} else {
if let dest = model.destination {
if let i = nav.viewControllers.lastIndex(of: dest) {
nav.setViewControllers(.init(nav.viewControllers.prefix(i + 1)), animated: true)
}
model.destination = nil
}
}
if isActive != model.contains(nav: nav) { // detect pop
isActive = model.contains(nav: nav)
}
}
final class Model {
var destination: UIHostingController<Destination>?
func contains(nav: UINavigationController) -> Bool { destination.map { nav.viewControllers.contains($0) } ?? false }
}
func action() { isActive = true }
}
extension NavigationLink {
init<Destination: View, Label: View>(destination: #autoclosure #escaping () -> Destination, #ViewBuilder label: #escaping () -> Label) {
self.init(body: NavigationLinkImpl(destination: destination, label: label))
}
init<Destination: View, Label: View>(destination: #autoclosure #escaping () -> Destination, isActive: Binding<Bool>, #ViewBuilder label: #escaping () -> Label) {
self.init(body: NavigationLinkImpl1(destination: destination, isActive: isActive, label: label))
}
init<Destination: View>(_ text: String, destination: #autoclosure #escaping () -> Destination, isActive: Binding<Bool>) {
self.init(destination: destination(), isActive: isActive) { Text(text) }
}
init<Destination: View>(_ text: String, destination: #autoclosure #escaping () -> Destination) {
self.init(destination: destination()) { Text(text) }
}
}
Put this in a file, and your existing NavigationLinks will work just fine. Tested in ios 14 and 15
Like anybody else on iOS 14.5.1 my application is hit by this awful bug. I have more than 3 NavigationLinks in the page, and I was not lucky to modify the numbers of the NavigationLinks (by adding a dummy NavigationLink) to get the correct behaviour.
A workaround that is Okay for me is to add a NavigationLink conditionally into the view.
Instead of:
var body: some View {
NavigationLink(destination: AnotherView(), isActive: $someCondition) { EmptyView() }
}
I have this:
var body: some View {
if someCondition {
NavigationLink(destination: AnotherView(), isActive: $someCondition) { EmptyView() }
}
}
The behaviour is not exactly the same, as you lose some navigation animation candy, but at least you have a working application again with relatively easy to understand fix.
You can also short-circuit it to 14.5 only, and normal behaviour elsewhere:
/// Assumes this gets fixed by Apple until 14.6 is out
var onIOS14_5: Bool {
let systemVersion = UIDevice.current.systemVersion
return systemVersion.starts(with: "14.5")
}
var body: some View {
if !onIOS14_5 || someCondition {
NavigationLink(destination: AnotherView(), isActive: $someCondition) { EmptyView() }
}
}
Perhaps this helps someone and lets all hope Apple will fix this embarrasing bug. Now I want my half day back.
In my case, the NavigationLink didn't work because of an .onTapGesture I added to dismiss the keyboard.
I got exactly the same problem.
my code:
class NavigationManager: ObservableObject {
static let shared: NavigationManager = {
return NavigationManager()
}()
#Published var showingMain: Bool
#Published var showingSub: Bool
#Published var content: AnyView
init() {
showingMain = false
showingSub = false
content = AnyView(EmptyView())
}
func forward<T:View>(content: #escaping () -> T ) {
showView()
self.content = AnyView(content())
}
private func showView() {
if !showingMain,!showingSub {
showingMain = true
} else if showingMain,!showingSub {
showingSub = true
} else if !showingMain,showingSub {
showingMain = true
}
}
}
struct NavigationLinkGroup: View {
#EnvironmentObject var navigationManager: NavigationManager
var body: some View {
Group {
NavigationLink(destination: navigationManager.content, isActive: $navigationManager.showingMain) {EmptyView()}
NavigationLink(destination: navigationManager.content, isActive: $navigationManager.showingSub) {EmptyView()}
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLinkGroup()
}
}
}
https://github.com/Ftrybe/CustomBackButtonOfSwiftUIApp/tree/master/CustomBackButtonOfSwiftUI
It seems that if there's more than one NavigationLink in NavigationView, this bug will be filed.
Here's my solution.
import SwiftUI
enum MyLink {
case myView1
case myView2
}
struct MyView1: View {
var body: some View {
Text("MyView1")
}
}
struct MyView2: View {
var body: some View {
Text("MyView2")
}
}
struct ExampleView: View {
#State var currentLink: MyLink = .myView1
#State var isLinkViewShow: Bool = false
func getLinkView(_ myLink: MyLink) -> some View {
if myLink == .myView1 {
return AnyView(MyView1())
} else {
return AnyView(MyView2())
}
}
var body: some View {
NavigationView {
VStack {
NavigationLink("",
destination: getLinkView(currentLink),
isActive: $isLinkViewShow)
// Press to navigate to MyView1
Button(action: {
currentLink = .myView1
isLinkViewShow = true
}) {
Text("To MyView1")
}
// Press to navigate to MyView2
Button(action: {
currentLink = .myView2
isLinkViewShow = true
}) {
Text("To MyView2")
}
}
}
}
}
Adding a delay gets auto-navigation working again.
NavigationLink(destination: PopupView(),isActive: $showView){}
&
.onAppear {
if (test()){
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {showView = true}
}
}

UIViewControllerRepresentable not correctly taking up space

I am trying to use a custom UIViewController in a SwiftUI view. I set up a UIViewControllerRepresentable class which creates the UIViewController in the makeUIViewController method. This creates the UIViewController and displays the button, however, the UIViewControllerRepresentable does not take up any space.
I tried using a UIImagePickerController instead of my custom controller, and that sizes correctly. The only way I got my controller to take up space was by setting a fixed frame on the UIViewControllerRepresentable in my SwiftUI view, which I absolutely don't want to do.
Note: I do need to use a UIViewController because I am trying to implement a UIMenuController in SwiftUI. I got all of it to work besides this problem I am having with it not sizing correctly.
Here is my code:
struct ViewControllerRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> MenuViewController {
let controller = MenuViewController()
return controller
}
func updateUIViewController(_ uiViewController: MenuViewController, context: Context) {
}
}
class MenuViewController: UIViewController {
override func viewDidLoad() {
let button = UIButton()
button.setTitle("Test button", for: .normal)
button.setTitleColor(.red, for: .normal)
self.view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
button.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
button.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
button.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
}
}
My SwiftUI view:
struct ClientView: View {
var body: some View {
VStack(spacing: 0) {
EntityViewItem(copyValue: "copy value", label: {
Text("Name")
}, content: {
Text("Random name")
})
.border(Color.green)
ViewControllerRepresentable()
.border(Color.red)
EntityViewItem(copyValue: "copy value", label: {
Text("Route")
}, content: {
HStack(alignment: .center) {
Text("Random route name")
}
})
.border(Color.blue)
}
}
}
Screenshot:
I do not have much experience with UIKit - my only experience is writing UIKit views to use in SwiftUI. The problem could very possibly be related to my lack of UIKit knowledge.
Thanks in advance!
Edit:
Here is the code for EntityViewItem. I will also provide the container view that ClientView is in - EntityView.
I also cleaned up the rest of the code and replaced references to Entity with hardcoded values.
struct EntityViewItem<Label: View, Content: View>: View {
var copyValue: String
var label: Label
var content: Content
var action: (() -> Void)?
init(copyValue: String, #ViewBuilder label: () -> Label, #ViewBuilder content: () -> Content, action: (() -> Void)? = nil) {
self.copyValue = copyValue
self.label = label()
self.content = content()
self.action = action
}
var body: some View {
VStack(alignment: .leading, spacing: 2) {
label
.opacity(0.6)
content
.onTapGesture {
guard let unwrappedAction = action else {
return
}
unwrappedAction()
}
.contextMenu {
Button(action: {
UIPasteboard.general.string = copyValue
}) {
Text("Copy to clipboard")
Image(systemName: "doc.on.doc")
}
}
}
.padding([.top, .leading, .trailing])
.frame(maxWidth: .infinity, alignment: .leading)
}
}
The container of ClientView:
struct EntityView: View {
let headerHeight: CGFloat = 56
var body: some View {
ZStack {
ScrollView(showsIndicators: false) {
VStack(spacing: 0) {
Color.clear.frame(
height: headerHeight
)
ClientView()
}
}
VStack(spacing: 0) {
HStack {
Button(action: {
}, label: {
Text("Back")
})
Spacer()
Text("An entity name")
.lineLimit(1)
.minimumScaleFactor(0.5)
Spacer()
Color.clear
.frame(width: 24, height: 0)
}
.frame(height: headerHeight)
.padding(.leading)
.padding(.trailing)
.background(
Color.white
.ignoresSafeArea()
.opacity(0.95)
)
Spacer()
}
}
}
}
If anyone else is trying to find an easier solution, that takes any view controller and resizes to fit its content:
struct ViewControllerContainer: UIViewControllerRepresentable {
let content: UIViewController
init(_ content: UIViewController) {
self.content = content
}
func makeUIViewController(context: Context) -> UIViewController {
let size = content.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
content.preferredContentSize = size
return content
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}
And then, when you use it in SwiftUI, make sure to call .fixedSize():
struct MainView: View {
var body: some View {
VStack(spacing: 0) {
ViewControllerContainer(MenuViewController())
.fixedSize()
}
}
}
Thanks so much to #udbhateja and #jnpdx for the help. That makes a lot of sense why the UIViewControllerRepresentable compresses its frame when inside a ScrollView. I did end up figuring out a solution to my problem which involved setting a fixed height on the UIViewControllerRepresentable. Basically, I used a PreferenceKey to find the height of the SwiftUI view, and set the frame of the UIViewControllerRepresentable to match it.
In case anyone has this same problem, here is my code:
struct EntityViewItem<Label: View, Content: View>: View {
var copyValue: String
var label: Label
var content: Content
var action: (() -> Void)?
#State var height: CGFloat = 0
init(copyValue: String, #ViewBuilder label: () -> Label, #ViewBuilder content: () -> Content, action: (() -> Void)? = nil) {
self.copyValue = copyValue
self.label = label()
self.content = content()
self.action = action
}
var body: some View {
ViewControllerRepresentable(copyValue: copyValue) {
SizingView(height: $height) { // This calculates the height of the SwiftUI view and sets the binding
VStack(alignment: .leading, spacing: 2) {
// Content
}
.padding([.leading, .trailing])
.padding(.top, 10)
.padding(.bottom, 10)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.frame(height: height) // Here I set the height to the value returned from the SizingView
}
}
And the code for SizingView:
struct SizingView<T: View>: View {
let view: T
#Binding var height: CGFloat
init(height: Binding<CGFloat>, #ViewBuilder view: () -> T) {
self.view = view()
self._height = height
}
var body: some View {
view.background(
GeometryReader { proxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: proxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self) { preferences in
height = preferences.height
}
}
func size(with view: T, geometry: GeometryProxy) -> T {
height = geometry.size.height
return view
}
}
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
With this finished, my UIMenuController is fully functional. It was a lot of code (if this functionality existed in SwiftUI, I probably would have had to write like 5 lines of code), but it works great. If anyone would like the code, please comment and I will share.
Here is an image of the final product:
As #jnpdx mentioned, you need to provide explicit size via frame for the representable to be visible as it's nested in VStack with other View.
If you have a specific reason to use UIViewController, then do provide explicit frame or else create a SwiftUI View.
struct ClientView: View {
var body: some View {
VStack(spacing: 0) {
EntityViewItem(copyValue: "copy value", label: {
Text("Name")
}, content: {
Text("Random name")
})
.border(Color.green)
ViewControllerRepresentable()
.border(Color.red)
.frame(height: 100.0)
EntityViewItem(copyValue: "copy value", label: {
Text("Route")
}, content: {
HStack(alignment: .center) {
Text("Random route name")
}
})
.border(Color.blue)
}
}
}
For anyone looking for the simplest possible solution, it's a couple of lines in #Edudjr's answer:
let size = content.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
content.preferredContentSize = size
Just add that inside your makeUIViewController!

SwiftUI: Custom button does not recognize touch with clear background and buttonStyle

I stumbled upon a weird behaviour for Buttons in SwiftUI in combination with a custom ButtonStyle.
My target was to create a custom ButtonStyle with some kind of 'push-back animation'. I used the following setup for this:
struct CustomButton<Content: View>: View {
private let content: () -> Content
init(content: #escaping () -> Content) {
self.content = content
}
var body: some View {
VStack {
Button(action: { ... }) {
content()
}
.buttonStyle(PushBackButtonStyle(pushBackScale: 0.9))
}
}
}
private struct PushBackButtonStyle: ButtonStyle {
let pushBackScale: CGFloat
func makeBody(configuration: Self.Configuration) -> some View {
configuration
.label
.scaleEffect(configuration.isPressed ? pushBackScale : 1.0)
}
}
// Preview
struct Playground_Previews: PreviewProvider {
static var previews: some View {
CustomButton {
VStack(spacing: 10) {
HStack {
Text("Button Text").background(Color.orange)
}
Divider()
HStack {
Text("Detail Text").background(Color.orange)
}
}
}
.background(Color.red)
}
}
When I now try to touch on this button outside of the Text view, nothing will happen. No animation will be visible and the action block will not be called.
What I found out so far:
when you remove the .buttonStyle(...) it does work as expected (no custom animation of course)
or when you set a .background(Color.red)) on the VStack in the CustomButton it does also work as expected in combination with the .buttonStyle(...)
The question now is if anybody have a better idea of how to properly work around this issue or how to fix it?
Just add hit testing content shape in your custom button style, like below
Tested with Xcode 11.4 / iOS 13.4
private struct PushBackButtonStyle: ButtonStyle {
let pushBackScale: CGFloat
func makeBody(configuration: Self.Configuration) -> some View {
configuration
.label
.contentShape(Rectangle()) // << fix !!
.scaleEffect(configuration.isPressed ? pushBackScale : 1.0)
}
}
Simply use a .frame and it should work.
To make it easily testable I have rewritten it like this:
struct CustomButton: View {
var body: some View {
Button(action: { }) {
VStack(spacing: 10) {
HStack {
Text("Button Text").background(Color.orange)
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.orange)
}
Divider()
HStack {
Text("Detail Text").background(Color.orange)
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.orange)
}
}
}
.buttonStyle(PushBackButtonStyle(pushBackScale: 0.9))
}
}
private struct PushBackButtonStyle: ButtonStyle {
let pushBackScale: CGFloat
func makeBody(configuration: Self.Configuration) -> some View {
configuration
.label
.scaleEffect(configuration.isPressed ? pushBackScale : 1.0)
}
}
I hope I could help. :-)
#Edit With video.

How to scroll List programmatically in SwiftUI?

It looks like in current tools/system, just released Xcode 11.4 / iOS 13.4, there will be no SwiftUI-native support for "scroll-to" feature in List. So even if they, Apple, will provide it in next major released, I will need backward support for iOS 13.x.
So how would I do it in most simple & light way?
scroll List to end
scroll List to top
and others
(I don't like wrapping full UITableView infrastructure into UIViewRepresentable/UIViewControllerRepresentable as was proposed earlier on SO).
SWIFTUI 2.0
Here is possible alternate solution in Xcode 12 / iOS 14 (SwiftUI 2.0) that can be used in same scenario when controls for scrolling is outside of scrolling area (because SwiftUI2 ScrollViewReader can be used only inside ScrollView)
Note: Row content design is out of consideration scope
Tested with Xcode 12b / iOS 14
class ScrollToModel: ObservableObject {
enum Action {
case end
case top
}
#Published var direction: Action? = nil
}
struct ContentView: View {
#StateObject var vm = ScrollToModel()
let items = (0..<200).map { $0 }
var body: some View {
VStack {
HStack {
Button(action: { vm.direction = .top }) { // < here
Image(systemName: "arrow.up.to.line")
.padding(.horizontal)
}
Button(action: { vm.direction = .end }) { // << here
Image(systemName: "arrow.down.to.line")
.padding(.horizontal)
}
}
Divider()
ScrollViewReader { sp in
ScrollView {
LazyVStack {
ForEach(items, id: \.self) { item in
VStack(alignment: .leading) {
Text("Item \(item)").id(item)
Divider()
}.frame(maxWidth: .infinity).padding(.horizontal)
}
}.onReceive(vm.$direction) { action in
guard !items.isEmpty else { return }
withAnimation {
switch action {
case .top:
sp.scrollTo(items.first!, anchor: .top)
case .end:
sp.scrollTo(items.last!, anchor: .bottom)
default:
return
}
}
}
}
}
}
}
}
SWIFTUI 1.0+
Here is simplified variant of approach that works, looks appropriate, and takes a couple of screens code.
Tested with Xcode 11.2+ / iOS 13.2+ (also with Xcode 12b / iOS 14)
Demo of usage:
struct ContentView: View {
private let scrollingProxy = ListScrollingProxy() // proxy helper
var body: some View {
VStack {
HStack {
Button(action: { self.scrollingProxy.scrollTo(.top) }) { // < here
Image(systemName: "arrow.up.to.line")
.padding(.horizontal)
}
Button(action: { self.scrollingProxy.scrollTo(.end) }) { // << here
Image(systemName: "arrow.down.to.line")
.padding(.horizontal)
}
}
Divider()
List {
ForEach(0 ..< 200) { i in
Text("Item \(i)")
.background(
ListScrollingHelper(proxy: self.scrollingProxy) // injection
)
}
}
}
}
}
Solution:
Light view representable being injected into List gives access to UIKit's view hierarchy. As List reuses rows there are no more values then fit rows into screen.
struct ListScrollingHelper: UIViewRepresentable {
let proxy: ListScrollingProxy // reference type
func makeUIView(context: Context) -> UIView {
return UIView() // managed by SwiftUI, no overloads
}
func updateUIView(_ uiView: UIView, context: Context) {
proxy.catchScrollView(for: uiView) // here UIView is in view hierarchy
}
}
Simple proxy that finds enclosing UIScrollView (needed to do once) and then redirects needed "scroll-to" actions to that stored scrollview
class ListScrollingProxy {
enum Action {
case end
case top
case point(point: CGPoint) // << bonus !!
}
private var scrollView: UIScrollView?
func catchScrollView(for view: UIView) {
if nil == scrollView {
scrollView = view.enclosingScrollView()
}
}
func scrollTo(_ action: Action) {
if let scroller = scrollView {
var rect = CGRect(origin: .zero, size: CGSize(width: 1, height: 1))
switch action {
case .end:
rect.origin.y = scroller.contentSize.height +
scroller.contentInset.bottom + scroller.contentInset.top - 1
case .point(let point):
rect.origin.y = point.y
default: {
// default goes to top
}()
}
scroller.scrollRectToVisible(rect, animated: true)
}
}
}
extension UIView {
func enclosingScrollView() -> UIScrollView? {
var next: UIView? = self
repeat {
next = next?.superview
if let scrollview = next as? UIScrollView {
return scrollview
}
} while next != nil
return nil
}
}
Just scroll to the id:
scrollView.scrollTo(ROW-ID)
Since SwiftUI structured designed Data-Driven, You should know all of your items IDs. So you can scroll to any id with ScrollViewReader from iOS 14 and with Xcode 12
struct ContentView: View {
let items = (1...100)
var body: some View {
ScrollViewReader { scrollProxy in
ScrollView {
ForEach(items, id: \.self) { Text("\($0)"); Divider() }
}
HStack {
Button("First!") { withAnimation { scrollProxy.scrollTo(items.first!) } }
Button("Any!") { withAnimation { scrollProxy.scrollTo(50) } }
Button("Last!") { withAnimation { scrollProxy.scrollTo(items.last!) } }
}
}
}
}
Note that ScrollViewReader should support all scrollable content, but now it only supports ScrollView
Preview
Preferred way
This answer is getting more attention, but I should state that the ScrollViewReader is the right way to do this. The introspect way is only if the reader/proxy doesn't work for you, because of a version restrictions.
ScrollViewReader { proxy in
ScrollView(.vertical) {
TopView().id("TopConstant")
...
MiddleView().id("MiddleConstant")
...
Button("Go to top") {
proxy.scrollTo("TopConstant", anchor: .top)
}
.id("BottomConstant")
}
.onAppear{
proxy.scrollTo("MiddleConstant")
}
.onChange(of: viewModel.someProperty) { _ in
proxy.scrollTo("BottomConstant")
}
}
The strings should be defined in one place, outside of the body property.
Legacy answer
Here is a simple solution that works on iOS13&14:
Using Introspect.
My case was for initial scroll position.
ScrollView(.vertical, showsIndicators: false, content: {
...
})
.introspectScrollView(customize: { scrollView in
scrollView.scrollRectToVisible(CGRect(x: 0, y: offset, width: 100, height: 300), animated: false)
})
If needed the height may be calculated from the screen size or the element itself.
This solution is for Vertical scroll. For horizontal you should specify x and leave y as 0
Thanks Asperi, great tip. I needed to have a List scroll up when new entries where added outside the view. Reworked to suit macOS.
I took the state/proxy variable to an environmental object and used this outside the view to force the scroll. I found I had to update it twice, the 2nd time with a .5sec delay to get the best result. The first update prevents the view from scrolling back to the top as the row is added. The 2nd update scrolls to the last row. I'm a novice and this is my first stackoverflow post :o
Updated for MacOS:
struct ListScrollingHelper: NSViewRepresentable {
let proxy: ListScrollingProxy // reference type
func makeNSView(context: Context) -> NSView {
return NSView() // managed by SwiftUI, no overloads
}
func updateNSView(_ nsView: NSView, context: Context) {
proxy.catchScrollView(for: nsView) // here NSView is in view hierarchy
}
}
class ListScrollingProxy {
//updated for mac osx
enum Action {
case end
case top
case point(point: CGPoint) // << bonus !!
}
private var scrollView: NSScrollView?
func catchScrollView(for view: NSView) {
//if nil == scrollView { //unB - seems to lose original view when list is emptied
scrollView = view.enclosingScrollView()
//}
}
func scrollTo(_ action: Action) {
if let scroller = scrollView {
var rect = CGRect(origin: .zero, size: CGSize(width: 1, height: 1))
switch action {
case .end:
rect.origin.y = scroller.contentView.frame.minY
if let documentHeight = scroller.documentView?.frame.height {
rect.origin.y = documentHeight - scroller.contentSize.height
}
case .point(let point):
rect.origin.y = point.y
default: {
// default goes to top
}()
}
//tried animations without success :(
scroller.contentView.scroll(to: NSPoint(x: rect.minX, y: rect.minY))
scroller.reflectScrolledClipView(scroller.contentView)
}
}
}
extension NSView {
func enclosingScrollView() -> NSScrollView? {
var next: NSView? = self
repeat {
next = next?.superview
if let scrollview = next as? NSScrollView {
return scrollview
}
} while next != nil
return nil
}
}
my two cents for deleting and repositioning list at any point based on other logic.. i.e. after delete/update, for example going to top.
(this is a ultra-reduced sample, I used this code after network call back to reposition: after network call I change previousIndex )
struct ContentView: View {
#State private var previousIndex : Int? = nil
#State private var items = Array(0...100)
func removeRows(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
self.previousIndex = offsets.first
}
var body: some View {
ScrollViewReader { (proxy: ScrollViewProxy) in
List{
ForEach(items, id: \.self) { Text("\($0)")
}.onDelete(perform: removeRows)
}.onChange(of: previousIndex) { (e: Equatable) in
proxy.scrollTo(previousIndex!-4, anchor: .top)
//proxy.scrollTo(0, anchor: .top) // will display 1st cell
}
}
}
}
This can now be simplified with all new ScrollViewProxy in Xcode 12, like so:
struct ContentView: View {
let itemCount: Int = 100
var body: some View {
ScrollViewReader { value in
VStack {
Button("Scroll to top") {
value.scrollTo(0)
}
Button("Scroll to buttom") {
value.scrollTo(itemCount-1)
}
ScrollView {
LazyVStack {
ForEach(0 ..< itemCount) { i in
Text("Item \(i)")
.frame(height: 50)
.id(i)
}
}
}
}
}
}
}
MacOS 11: In case you need to scroll a list based on input outside the view hierarchy. I have followed the original scroll proxy pattern using the new scrollViewReader:
struct ScrollingHelperInjection: NSViewRepresentable {
let proxy: ScrollViewProxy
let helper: ScrollingHelper
func makeNSView(context: Context) -> NSView {
return NSView()
}
func updateNSView(_ nsView: NSView, context: Context) {
helper.catchProxy(for: proxy)
}
}
final class ScrollingHelper {
//updated for mac os v11
private var proxy: ScrollViewProxy?
func catchProxy(for proxy: ScrollViewProxy) {
self.proxy = proxy
}
func scrollTo(_ point: Int) {
if let scroller = proxy {
withAnimation() {
scroller.scrollTo(point)
}
} else {
//problem
}
}
}
Environmental object:
#Published var scrollingHelper = ScrollingHelper()
In the view: ScrollViewReader { reader in .....
Injection in the view:
.background(ScrollingHelperInjection(proxy: reader, helper: scrollingHelper)
Usage outside the view hierarchy: scrollingHelper.scrollTo(3)
As mentioned in #lachezar-todorov's answer Introspect is a nice library to access UIKit elements in SwiftUI. But be aware that the block you use for accessing UIKit elements are being called multiple times. This can really mess up your app state. In my cas CPU usage was going %100 and app was getting unresponsive. I had to use some pre conditions to avoid it.
ScrollView() {
...
}.introspectScrollView { scrollView in
if aPreCondition {
//Your scrolling logic
}
}
Another cool way is to just use namespace wrappers:
A dynamic property type that allows access to a namespace defined by the persistent identity of the object containing the property (e.g. a view).
struct ContentView: View {
#Namespace private var topID
#Namespace private var bottomID
let items = (0..<100).map { $0 }
var body: some View {
ScrollView {
ScrollViewReader { proxy in
Section {
LazyVStack {
ForEach(items.indices, id: \.self) { index in
Text("Item \(items[index])")
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.green.cornerRadius(16))
}
}
} header: {
HStack {
Text("header")
Spacer()
Button(action: {
withAnimation {
proxy.scrollTo(bottomID)
}
}
) {
Image(systemName: "arrow.down.to.line")
.padding(.horizontal)
}
}
.padding(.vertical)
.id(topID)
} footer: {
HStack {
Text("Footer")
Spacer()
Button(action: {
withAnimation {
proxy.scrollTo(topID) }
}
) {
Image(systemName: "arrow.up.to.line")
.padding(.horizontal)
}
}
.padding(.vertical)
.id(bottomID)
}
.padding()
}
}
.foregroundColor(.white)
.background(.black)
}
}
Two parts:
Wrap the List (or ScrollView) with ScrollViewReader
Use the scrollViewProxy (that comes from ScrollViewReader) to scroll to an id of an element in the List. You can seemingly use EmptyView().
The example below uses a notification for simplicity (use a function if you can instead!).
ScrollViewReader { scrollViewProxy in
List {
EmptyView().id("top")
}
.onReceive(NotificationCenter.default.publisher(for: .ScrollToTop)) { _ in
// when using an anchor of `.top`, it failed to go all the way to the top
// so here we add an extra -50 so it goes to the top
scrollViewProxy.scrollTo("top", anchor: UnitPoint(x: 0, y: -50))
}
}
extension Notification.Name {
static let ScrollToTop = Notification.Name("ScrollToTop")
}
NotificationCenter.default.post(name: .ScrollToTop, object: nil)

Tap Action not working when Color is clear SwiftUI

my tapAction is not recognizing a tap when my foregroundColor is clear. When i remove the color it works fine.
That's my code:
ZStack {
RoundedRectangle(cornerRadius: 0)
.foregroundColor(Color.clear)
.frame(width: showMenu ? UIScreen.main.bounds.width : 0)
.tapAction {
self.showMenu.toggle()
}
RoundedRectangle(cornerRadius: 5)
.foregroundColor(Color.green)
.shadow(radius: 5, y: 2)
.padding(.trailing, 50)
.frame(width: showMenu ? UIScreen.main.bounds.width : 0)
}
.edgesIgnoringSafeArea(.top)
The accurate way is to use .contentShape(Rectangle()) on the view.
Described in this tutorial:
control-the-tappable-area-of-a-view by Paul Hudson #twostraws
VStack {
Image("Some Image").resizable().frame(width: 50, height: 50)
Spacer().frame(height: 50)
Text("Some Text")
}
.contentShape(Rectangle())
.onTapGesture {
print("Do Something")
}
how-to-control-the-tappable-area-of-a-view-using-contentshape stackoverflow
I have also discovered that a shape filled with Color.clear does not generate a tappable area.
Here are two workarounds:
Use Color.black.opacity(0.0001) (even on 10-bits-per-channel displays). This generates a color that is so transparent that it should have no effect on your appearance, and generates a tappable area that fills its frame. I don't know if SwiftUI is smart enough to skip rendering the color, so I don't know if it has any performance impact.
Use a GeometryReader to get the frame size, and then use the contentShape to generate the tappable area:
GeometryReader { proxy in
Color.clear.contentShape(Path(CGRect(origin: .zero, size: proxy.size)))
}
Here is the component
struct InvisibleButton: View {
let action: (() -> Void)?
var body: some View {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
action?()
}
}
}
usage: Put your view and InbisibleButton in ZStack
ZStack {
**yourView()**
InvisibleButton {
print("Invisible button tapped")
}
}
you also can make a modifier to simplify usage:
struct InvisibleButtonModifier: ViewModifier {
let action: (() -> Void)?
func body(content: Content) -> some View {
ZStack {
content
InvisibleButton(action: action)
}
}
}
**yourView()**
.modifier(InvisibleButtonModifier {
print("Invisible button tapped")
})
However, if your SwiftUI View has a UIKit view as a subview under, you will have to set Color.gray.opacity(0.0001) in order to UIView's touches be ignored
In my case a View that didn't trigger onTapGesture:
struct MainView: View {
var action: () -> Void
var body: some View {
NotTappableView()
.contentShape(Rectangle())
.onTapGesture(
action()
)
}
}
I solved this way:
struct MainView: View {
var action: () -> Void
var body: some View {
NotTappableView()
.overlay(
Color.clear
.contentShape(Rectangle())
.onTapGesture {
action()
}
)
}
}
This made whole untappable view now tappable.

Resources