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.
Related
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)
}
}
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.
I have a Picker in my SwiftUI View with the new MenuPickerStyle.
As you can see, the label of the picker is same of the options, and it becomes dim when changing from one option to another.
It looks like it is not tappable, but when tapping it does the required job.
Here's my code. It is just a very simple picker implementation.
struct MenuPicker: View {
#State var selection: String = "one"
var array: [String] = ["one", "two", "three", "four", "five"]
var body: some View {
Picker(selection: $selection, label: Text(selection).frame(width: 100), content: {
ForEach(array, id: \.self, content: { word in
Text(word).tag(word)
})
})
.pickerStyle(MenuPickerStyle())
.padding()
.background(Color(.systemBackground).edgesIgnoringSafeArea(.all))
.cornerRadius(5)
}
}
struct ContentView: View {
var body: some View {
ZStack {
Color.gray
MenuPicker()
}
}
}
It looks like it's a bug with:
public init(selection: Binding<SelectionValue>, label: Label, #ViewBuilder content: () -> Content)
You can try replacing it with:
public init(_ titleKey: LocalizedStringKey, selection: Binding<SelectionValue>, #ViewBuilder content: () -> Content)
Here is a workaround (you can only use String as the label):
Picker(selection, selection: $selection) {
ForEach(array, id: \.self) { word in
Text(word).tag(word)
}
}
.frame(width: 100)
.animation(nil)
.pickerStyle(MenuPickerStyle())
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?
How can I dismiss the keyboard after the user clicks outside the TextField using SwiftUI?
I created a TextField using SwiftUI, but I couldn't find any solution for dismissing the keyboard if the user clicks outside the TextField. I took a look at all attributes of TextField and also the SwiftUI TextField documentation and I couldn't find anything related with dismissing keyboard.
This is my view's code:
struct InputView: View {
#State var inputValue : String = ""
var body: some View {
VStack(spacing: 10) {
TextField("$", text: $inputValue)
.keyboardType(.decimalPad)
}
}
}
This can be done with a view modifier.
Code
public extension View {
func dismissKeyboardOnTap() -> some View {
modifier(DismissKeyboardOnTap())
}
}
public struct DismissKeyboardOnTap: ViewModifier {
public func body(content: Content) -> some View {
#if os(macOS)
return content
#else
return content.gesture(tapGesture)
#endif
}
private var tapGesture: some Gesture {
TapGesture().onEnded(endEditing)
}
private func endEditing() {
UIApplication.shared.connectedScenes
.filter {$0.activationState == .foregroundActive}
.map {$0 as? UIWindowScene}
.compactMap({$0})
.first?.windows
.filter {$0.isKeyWindow}
.first?.endEditing(true)
}
}
Usage
backgroundView()
.dismissKeyboardOnTap()
Check out the demo here: https://github.com/youjinp/SwiftUIKit
here is the solution using DragGesture it's working.
struct ContentView: View {
#State var text: String = ""
var body: some View {
VStack {
TextField("My Text", text: $text)
.keyboardType(.decimalPad)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
.gesture(
TapGesture()
.onEnded { _ in
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
)
}
}
Add tap gesture to most outer view and call extension method inside tap gesture closure.
struct InputView: View {
#State var inputValue : String = ""
var body: some View {
VStack(spacing: 10) {
TextField("$", text: $inputValue)
.keyboardType(.decimalPad)
} .onTapGesture(perform: {
self.endTextEditing()
})
}
}
extension View {
func endTextEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
TextField("Phone Number", text: $no)
.keyboardType(.numbersAndPunctuation)
.padding()
.background(Color("4"))
.clipShape(RoundedRectangle(cornerRadius: 10))
.offset(y:-self.value).animation(.spring()).onAppear() {
NotificationCenter.default.addObserver(forName:UIResponder.keyboardWillShowNotification, object: nil, queue: .main){ (notif)in
let value = notif.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
let height = value.height
self.value = height
}
NotificationCenter.default.addObserver(forName:UIResponder.keyboardWillHideNotification, object: nil, queue: .main){ (notification)in
self.value = 0
}
}
In SwiftUI 3 #FocusState wrapper can be used to remove or switch focus from editable fields. When the focus is removed from field, keyboard dismisses. So in your case it is just a matter of giving space and gesture to the surrounding space of TextView.
struct ContentView: View {
#State var inputValue : String = ""
#FocusState private var inputIsFocused: Bool
var body: some View {
VStack(spacing: 10) {
TextField("$", text: $inputValue)
.keyboardType(.decimalPad)
.border(Color.green)
.focused($inputIsFocused)
}
.frame(maxHeight: .infinity) // If input is supposed to be in the center
.background(.yellow)
.onTapGesture {
inputIsFocused = false
}
}
}
But we can do more interesting things with #FocusState. How about switching from field to field in a form. And if you tap away, keyboard also dismisses.
struct ContentView: View {
enum Field {
case firstName
case lastName
case emailAddress
}
#State private var firstName = ""
#State private var lastName = ""
#State private var emailAddress = ""
#FocusState private var focusedField: Field?
var body: some View {
ZStack {
VStack {
TextField("Enter first name", text: $firstName)
.focused($focusedField, equals: .firstName)
.textContentType(.givenName)
.submitLabel(.next)
TextField("Enter last name", text: $lastName)
.focused($focusedField, equals: .lastName)
.textContentType(.familyName)
.submitLabel(.next)
TextField("Enter email address", text: $emailAddress)
.focused($focusedField, equals: .emailAddress)
.textContentType(.emailAddress)
.submitLabel(.join)
}
.onSubmit {
switch focusedField {
case .firstName:
focusedField = .lastName
case .lastName:
focusedField = .emailAddress
default:
print("Creating account…")
}
}
}
.textFieldStyle(.roundedBorder)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle()) // So ZStack becomes clickable
.onTapGesture {
focusedField = nil
}
}
}