SwiftUI NavigationLink in ScrollView not responding until scrolling - ios

As the title says, NavigationLink (or Button) does not receive tap before the horizontal scrollview has been scrolled, i.e. nothing is tappable/reponsive. After scrolling everything works as expected.
I am using Xcode 12 Beta 2 with the API of iOS 13 (can not use iOS 14 API due to the clients needs)
struct ARQuickMenuChooseArt: View {
let title: String
let favorites: [Artwork]
var donePressed: (() -> Void)?
var seeAllArtPressed: (() -> Void)?
var body: some View {
VStack(alignment: .leading, spacing: 24) {
Text("Favourites")
.font(.title)
.padding(.bottom, -10)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(favorites) { favorite in
NavigationLink(destination: Text("Destination View")) {
ARQuickMenuArtListItem(artwork: favorite)
}
}
}
}
}
}
}
Here is the view that uses the view containing the scrollview.
struct AddArtFlowView: View {
let controller: ARArtworksModelController
#Binding var position: CardPosition
#EnvironmentObject var appState: AppState
var body: some View {
ARMenuContainer(position: $position, dismissAction: dismiss) {
NavigationView {
ARQuickMenuChooseArt(
title: "Add artwork",
favorites: artworkData,
seeAllArtPressed: {
appState.activeView = .menuOpen(.add)
}
)
}
}
}
}
ARMenuContainer is simply a wrapper, seen here:
struct ARMenuContainer<Content>: View where Content: View {
#Binding var position: CardPosition
let dismissAction: () -> Void
let content: () -> Content
init(position: Binding<CardPosition>, dismissAction: #escaping () -> Void, #ViewBuilder content: #escaping () -> Content) {
self._position = position
self.dismissAction = dismissAction
self.content = content
}
var body: some View {
SlideOverCard($position, backgroundTapped: dismissAction) {
VStack {
content()
Spacer()
}
.padding(.top, -3)
}
}
}
Which in turn uses a SlideOverCard https://github.com/moifort/swiftUI-slide-over-card, which is used to display above an AR experience.
Here is how it looks in-app
Any ideas why it will not respond without first scrolling?

Related

SwiftUI custom TabView with paging style

I'm trying to create a custom TabView in SwiftUI, that also has a .tabViewStyle(.page()) functionality too.
At the moment I'm 99% of the way there, but cannot figure out how to get all the TabBarItems to list.
I'm using the PreferenceKey so that the order I add them into the closure is the order in the TabView.
When I run it, the tab items are added to the array, then removed, and it doesn't seem to be working.
I had it working with the enum as CaseIterable and the ForEach(tabs) { tab in as ForEach(TabBarItems.allCases) { tab in, but as mentioned wanted the order in the bar to be organic from the clousure.
Container
struct TabViewContainer<Content : View>: View {
#Binding private var selection: TabBarItem
#State private var tabs: [TabBarItem] = []
var content: Content
init(selection: Binding<TabBarItem>, #ViewBuilder content: () -> Content) {
self._selection = selection
self.content = content()
}
var body: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selection) {
content
}
.tabViewStyle(.page(indexDisplayMode: .never))
tabBarItems()
}
.onPreferenceChange(TabBarItemsPreferenceKey.self) { self.tabs = $0 }
}
private func tabBarItems() -> some View {
HStack(spacing: 10) {
ForEach(tabs) { tab in
Button {
selection = tab
} label: {
tabButton(tab: tab)
}
}
}
.padding(.horizontal)
.frame(maxWidth: .infinity)
.padding(.top, 8)
.background(Color(uiColor: .systemGray6))
}
private func tabButton(tab: TabBarItem) -> some View {
VStack(spacing: 0) {
Image(icon: tab.icon)
.font(.system(size: 16))
.frame(maxWidth: .infinity, minHeight: 28)
Text(tab.title)
.font(.system(size: 10, weight: .medium, design: .rounded))
}
.foregroundColor(selection == tab ? tab.colour : .gray)
}
}
PreferenceKey / Modifier
struct TabBarItemsPreferenceKey: PreferenceKey {
static var defaultValue: [TabBarItem] = []
static func reduce(value: inout [TabBarItem], nextValue: () -> [TabBarItem]) {
value += nextValue()
}
}
struct TabBarItemViewModifier: ViewModifier {
let tab: TabBarItem
func body(content: Content) -> some View {
content.preference(key: TabBarItemsPreferenceKey.self, value: [tab])
}
}
extension View {
func tabBarItem(_ tab: TabBarItem) -> some View {
modifier(TabBarItemViewModifier(tab: tab))
}
}
Demo view
struct TabSelectionView: View {
#State private var selection: TabBarItem = .itinerary
var body: some View {
TabViewContainer(selection: $selection) {
PhraseView()
.tabBarItem(.phrases)
ItineraryView()
.tabBarItem(.itinerary)
BudgetView()
.tabBarItem(.budget)
BookingView()
.tabBarItem(.bookings)
PackingListView()
.tabBarItem(.packing)
}
}
}
Intended
Current
You can use a more elegant way, #resultBuilder:
You create a struct that holds the View & the tag;
tabBarItem should now return the previously created struct;
The #resultBuilder will then build your array of your view & tag which you'll be using inside the container.
ResultBuilder:
#resultBuilder
public struct TabsBuilder {
internal static func buildBlock(_ components: Tab...) -> [Tab] {
return components
}
internal static func buildEither(first component: Tab) -> Tab {
return component
}
internal static func buildEither(second component: Tab) -> Tab {
return component
}
}
Tab:
struct Tab: Identifiable {
var content: AnyView //I don't recommend the use of AnyView, but I don't want to dive deep into generics for now.
var tag: TabBarItem
var id = UUID()
}
Modifier:
struct Tab: Identifiable {
var content: AnyView
var tag: TabBarItem
var id = UUID()
}
TabViewContainer:
struct TabViewContainer: View {
#Binding private var selection: TabBarItem
#State private var tabs: [TabBarItem]
var content: [Tab]
init(selection: Binding<TabBarItem>, #TabsBuilder content: () -> [Tab]) {
self._selection = selection
self.content = content()
self.tabs = self.content.map({$0.tag})
}
var body: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selection) {
ForEach(content) { content in
content.content
.tag(content.tag)
}
}.tabViewStyle(.page(indexDisplayMode: .never))
tabBarItems()
}
}
private func tabBarItems() -> some View {
HStack(spacing: 10) {
ForEach(tabs) { tab in
Button {
selection = tab
} label: {
tabButton(tab: tab)
}
}
}
.padding(.horizontal)
.frame(maxWidth: .infinity)
.padding(.top, 8)
.background(Color(uiColor: .systemGray6))
}
private func tabButton(tab: TabBarItem) -> some View {
VStack(spacing: 0) {
Image(icon: tab.icon)
.font(.system(size: 16))
.frame(maxWidth: .infinity, minHeight: 28)
Text(tab.title)
.font(.system(size: 10, weight: .medium, design: .rounded))
}
.foregroundColor(selection == tab ? tab.colour : .gray)
}
}

Overlay in front of SwiftUI tab view

I'm using SwiftUI's TabView and I want to add a custom bottomSheet() modifier which accepts a view and displays it like the standard sheet() modifier, but without occupying the entire screen.
Current Behaviour: I managed to create the custom modifier and show a sheet, but the sheet comes up behind the bottom tab bar (since it is displayed from within the NavigationView).
Expected Behavior: I'm looking for a way to cover the tab bar with the sheet in front.
Minimal Reproducible Example
Here's the custom modifier that I've created.
struct BottomSheet<SheetContent: View>: ViewModifier {
let sheetContent: SheetContent
#Binding var isPresented: Bool
init(isPresented: Binding<Bool>, #ViewBuilder content: () -> SheetContent) {
self.sheetContent = content()
_isPresented = isPresented
}
func body(content: Content) -> some View {
ZStack {
content
if isPresented {
ZStack {
Color.black.opacity(0.1)
VStack {
Spacer()
sheetContent
.padding()
.frame(maxWidth: .infinity)
.background(
Rectangle()
.foregroundColor(.white)
)
}
}
}
}
}
}
extension View {
func bottomSheet<SheetContent: View>(isPresented: Binding<Bool>, #ViewBuilder content: #escaping () -> SheetContent) -> some View {
self.modifier(BottomSheet(isPresented: isPresented, content: content))
}
}
Here's how I'm using it.
struct ScheduleTab: View {
#State private var showSheet = false
var body: some View {
NavigationView {
Button("Open Sheet") {
showSheet.toggle()
}
}
.navigationTitle("Today")
.navigationBarTitleDisplayMode(.inline)
.bottomSheet(isPresented: $showSheet) {
Text("Hello, World")
}
}
}

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 - Changing GraphicalDatePickerStyle DatePicker's visibility causes memory leak

I have an app that has a datepicker (GraphicalDatePickerStyle). It hidden by default. When you tap the view that contains it, it becomes visible.
DateTimePicker
struct DateTimePicker<Content: View>: View {
#Binding var selection: Date
#State private var isDatePickerVisible: Bool = false
private var displayedComponents: DatePickerComponents
private var content: (_ isVisible: Bool) -> Content
init(selection: Binding<Date>,
displayedComponents: DatePickerComponents = [.date],
content: #escaping (_ isVisible: Bool) -> Content) {
self._selection = selection
self.content = content
self.displayedComponents = displayedComponents
}
var body: some View {
VStack(alignment: .center) {
self.content(isDatePickerVisible)
.onTapGesture {
withAnimation {
self.isDatePickerVisible.toggle()
}
}
if isDatePickerVisible {
VStack {
DatePicker("",
selection: $selection,
displayedComponents: self.displayedComponents)
.labelsHidden()
.datePickerStyle(GraphicalDatePickerStyle())
}
}
}
}
}
ContentView that contains DateTimePicker
struct ContentView: View {
#State var selection = Date()
var body: some View {
ScrollView(.vertical) {
DateTimePicker(selection: $selection) { _ in
HStack {
Text("Date ")
Spacer()
Text("\(selection)")
}
}
}
}
}
There are a few memory leaks if you show and hide DateTimePicker. I tried a lot but could not fix it.

ScrollView and .sheet

I have a ScrollView with a ForEach loop, each rendering a View. In the View I have 3 renders of the below ActionItem (a button that displays a sheet). The sheet does not show up with ScrollView but does with List. I'd normally attach the .sheet at the ScrollView layer however, with each button rendering a different view it seems more appropriate to nest it.
How I could get this to work with ScrollView? I'm using Xcode 12
struct ActionItem<Content>: View where Content : View {
public var text: String
public var icon: String
public var content: Content
#State var isPresented = false
init(text: String, icon: String, #ViewBuilder content: () -> Content) {
self.text = text
self.icon = icon
self.content = content()
}
var body: some View {
Button (action: {
DispatchQueue.main.async {
withAnimation {
self.isPresented = true
}
}
}) {
HStack(spacing: 2) {
Image(systemName: icon).font(.system(size: 14, weight: .semibold))
Text(text).fontWeight(.semibold)
}.padding([.top, .bottom], Dimensions.spacing)
.padding([.leading, .trailing], Dimensions.spacingMedium)
}.foregroundColor(Color.gray).font(.subheadline).background(Color.grayWhiteTer)
.cornerRadius(Dimensions.spacing)
.sheet(isPresented: $isPresented) {
self.content
}
}
}
In the View I'd render ActionItem such as Text, this also occurs if the View is ignored and the ActionItem is just directly in the ForEach. Same issue, sheet does not appear.
ActionItem(text: "", icon: "pencil") {
Text("ok")
}
The list looks like this
import SwiftUI
struct ItemsList: View {
#ObservedObject var itemModel: ItemModel
var body: some View {
VStack(alignment: .center, spacing: 0) {
ScrollView {
VStack {
ForEach(itemModel.items, id: \.self) { item in
ItemView(item: item)
}
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}}
Suggested callback update
struct ActionItem<Content>: View where Content : View {
public var text: String
public var icon: String
public var content: () -> Content
#State var isPresented = false
init(text: String, icon: String, #ViewBuilder content: #escaping () -> Content) {
self.text = text
self.icon = icon
self.content = content
}
var body: some View {
Button (action: {
DispatchQueue.main.async {
withAnimation {
self.isPresented = true
}
}
}) {
HStack(spacing: 2) {
Image(systemName: icon).font(.system(size: 14, weight: .semibold))
Text(text).fontWeight(.semibold)
}.padding([.top, .bottom], Dimensions.spacing)
.padding([.leading, .trailing], Dimensions.spacingMedium)
}.foregroundColor(Color.gray).font(.subheadline).background(Color.grayWhiteTer)
.cornerRadius(Dimensions.spacing)
.sheet(isPresented: $isPresented) {
self.content()
}
}
}
Try saving content as a callback (i.e. () -> Content) and call it in the sheet method instead of calling it in the initializer.. This will change when the view is created.

Resources